tabby-tmux 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js ADDED
@@ -0,0 +1,4226 @@
1
+ (function webpackUniversalModuleDefinition(root, factory) {
2
+ if(typeof exports === 'object' && typeof module === 'object')
3
+ module.exports = factory(require("@angular/core"), require("@angular/common"), require("@angular/forms"), require("tabby-core"), require("tabby-settings"), require("tabby-terminal"), require("rxjs"));
4
+ else if(typeof define === 'function' && define.amd)
5
+ define(["@angular/core", "@angular/common", "@angular/forms", "tabby-core", "tabby-settings", "tabby-terminal", "rxjs"], factory);
6
+ else {
7
+ var a = typeof exports === 'object' ? factory(require("@angular/core"), require("@angular/common"), require("@angular/forms"), require("tabby-core"), require("tabby-settings"), require("tabby-terminal"), require("rxjs")) : factory(root["@angular/core"], root["@angular/common"], root["@angular/forms"], root["tabby-core"], root["tabby-settings"], root["tabby-terminal"], root["rxjs"]);
8
+ for(var i in a) (typeof exports === 'object' ? exports : root)[i] = a[i];
9
+ }
10
+ })(this, (__WEBPACK_EXTERNAL_MODULE__angular_core__, __WEBPACK_EXTERNAL_MODULE__angular_common__, __WEBPACK_EXTERNAL_MODULE__angular_forms__, __WEBPACK_EXTERNAL_MODULE_tabby_core__, __WEBPACK_EXTERNAL_MODULE_tabby_settings__, __WEBPACK_EXTERNAL_MODULE_tabby_terminal__, __WEBPACK_EXTERNAL_MODULE_rxjs__) => {
11
+ return /******/ (() => { // webpackBootstrap
12
+ /******/ var __webpack_modules__ = ({
13
+
14
+ /***/ "./node_modules/.pnpm/css-loader@7.1.2_webpack@5.104.1/node_modules/css-loader/dist/cjs.js!./node_modules/.pnpm/sass-loader@16.0.6_sass@1.97.2_webpack@5.104.1/node_modules/sass-loader/dist/cjs.js!./src/components/settings.component.scss"
15
+ /*!***************************************************************************************************************************************************************************************************************************************************!*\
16
+ !*** ./node_modules/.pnpm/css-loader@7.1.2_webpack@5.104.1/node_modules/css-loader/dist/cjs.js!./node_modules/.pnpm/sass-loader@16.0.6_sass@1.97.2_webpack@5.104.1/node_modules/sass-loader/dist/cjs.js!./src/components/settings.component.scss ***!
17
+ \***************************************************************************************************************************************************************************************************************************************************/
18
+ (module, __webpack_exports__, __webpack_require__) {
19
+
20
+ "use strict";
21
+ __webpack_require__.r(__webpack_exports__);
22
+ /* harmony export */ __webpack_require__.d(__webpack_exports__, {
23
+ /* harmony export */ "default": () => (__WEBPACK_DEFAULT_EXPORT__)
24
+ /* harmony export */ });
25
+ /* harmony import */ var _node_modules_pnpm_css_loader_7_1_2_webpack_5_104_1_node_modules_css_loader_dist_runtime_sourceMaps_js__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! ../../node_modules/.pnpm/css-loader@7.1.2_webpack@5.104.1/node_modules/css-loader/dist/runtime/sourceMaps.js */ "./node_modules/.pnpm/css-loader@7.1.2_webpack@5.104.1/node_modules/css-loader/dist/runtime/sourceMaps.js");
26
+ /* harmony import */ var _node_modules_pnpm_css_loader_7_1_2_webpack_5_104_1_node_modules_css_loader_dist_runtime_sourceMaps_js__WEBPACK_IMPORTED_MODULE_0___default = /*#__PURE__*/__webpack_require__.n(_node_modules_pnpm_css_loader_7_1_2_webpack_5_104_1_node_modules_css_loader_dist_runtime_sourceMaps_js__WEBPACK_IMPORTED_MODULE_0__);
27
+ /* harmony import */ var _node_modules_pnpm_css_loader_7_1_2_webpack_5_104_1_node_modules_css_loader_dist_runtime_api_js__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(/*! ../../node_modules/.pnpm/css-loader@7.1.2_webpack@5.104.1/node_modules/css-loader/dist/runtime/api.js */ "./node_modules/.pnpm/css-loader@7.1.2_webpack@5.104.1/node_modules/css-loader/dist/runtime/api.js");
28
+ /* harmony import */ var _node_modules_pnpm_css_loader_7_1_2_webpack_5_104_1_node_modules_css_loader_dist_runtime_api_js__WEBPACK_IMPORTED_MODULE_1___default = /*#__PURE__*/__webpack_require__.n(_node_modules_pnpm_css_loader_7_1_2_webpack_5_104_1_node_modules_css_loader_dist_runtime_api_js__WEBPACK_IMPORTED_MODULE_1__);
29
+ // Imports
30
+
31
+
32
+ var ___CSS_LOADER_EXPORT___ = _node_modules_pnpm_css_loader_7_1_2_webpack_5_104_1_node_modules_css_loader_dist_runtime_api_js__WEBPACK_IMPORTED_MODULE_1___default()((_node_modules_pnpm_css_loader_7_1_2_webpack_5_104_1_node_modules_css_loader_dist_runtime_sourceMaps_js__WEBPACK_IMPORTED_MODULE_0___default()));
33
+ // Module
34
+ ___CSS_LOADER_EXPORT___.push([module.id, `.tmux-settings-tab .tmux-table {
35
+ width: 100%;
36
+ display: table;
37
+ border-spacing: 0px 20px;
38
+ }
39
+ .tmux-settings-tab .tmux-table .row {
40
+ width: 100%;
41
+ display: table-row;
42
+ }
43
+ .tmux-settings-tab .tmux-table .row .header {
44
+ padding: 0;
45
+ width: 12em;
46
+ display: table-cell;
47
+ }
48
+ .tmux-settings-tab .tmux-table .row .form-control {
49
+ width: 100%;
50
+ display: table-cell;
51
+ }`, "",{"version":3,"sources":["webpack://./src/components/settings.component.scss"],"names":[],"mappings":"AACE;EACE,WAAA;EACA,cAAA;EACA,wBAAA;AAAJ;AACI;EACE,WAAA;EACA,kBAAA;AACN;AAAM;EACE,UAAA;EACA,WAAA;EACA,mBAAA;AAER;AAAM;EACE,WAAA;EACA,mBAAA;AAER","sourcesContent":[".tmux-settings-tab {\n .tmux-table {\n width: 100%;\n display: table;\n border-spacing: 0px 20px;\n .row {\n width: 100%;\n display: table-row;\n .header {\n padding: 0;\n width: 12em;\n display: table-cell;\n }\n .form-control {\n width: 100%;\n display: table-cell;\n }\n }\n }\n}\n"],"sourceRoot":""}]);
52
+ // Exports
53
+ /* harmony default export */ const __WEBPACK_DEFAULT_EXPORT__ = (___CSS_LOADER_EXPORT___);
54
+
55
+
56
+ /***/ },
57
+
58
+ /***/ "./node_modules/.pnpm/css-loader@7.1.2_webpack@5.104.1/node_modules/css-loader/dist/cjs.js!./node_modules/.pnpm/sass-loader@16.0.6_sass@1.97.2_webpack@5.104.1/node_modules/sass-loader/dist/cjs.js!./src/components/tmuxPaneTab.component.scss"
59
+ /*!******************************************************************************************************************************************************************************************************************************************************!*\
60
+ !*** ./node_modules/.pnpm/css-loader@7.1.2_webpack@5.104.1/node_modules/css-loader/dist/cjs.js!./node_modules/.pnpm/sass-loader@16.0.6_sass@1.97.2_webpack@5.104.1/node_modules/sass-loader/dist/cjs.js!./src/components/tmuxPaneTab.component.scss ***!
61
+ \******************************************************************************************************************************************************************************************************************************************************/
62
+ (module, __webpack_exports__, __webpack_require__) {
63
+
64
+ "use strict";
65
+ __webpack_require__.r(__webpack_exports__);
66
+ /* harmony export */ __webpack_require__.d(__webpack_exports__, {
67
+ /* harmony export */ "default": () => (__WEBPACK_DEFAULT_EXPORT__)
68
+ /* harmony export */ });
69
+ /* harmony import */ var _node_modules_pnpm_css_loader_7_1_2_webpack_5_104_1_node_modules_css_loader_dist_runtime_sourceMaps_js__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! ../../node_modules/.pnpm/css-loader@7.1.2_webpack@5.104.1/node_modules/css-loader/dist/runtime/sourceMaps.js */ "./node_modules/.pnpm/css-loader@7.1.2_webpack@5.104.1/node_modules/css-loader/dist/runtime/sourceMaps.js");
70
+ /* harmony import */ var _node_modules_pnpm_css_loader_7_1_2_webpack_5_104_1_node_modules_css_loader_dist_runtime_sourceMaps_js__WEBPACK_IMPORTED_MODULE_0___default = /*#__PURE__*/__webpack_require__.n(_node_modules_pnpm_css_loader_7_1_2_webpack_5_104_1_node_modules_css_loader_dist_runtime_sourceMaps_js__WEBPACK_IMPORTED_MODULE_0__);
71
+ /* harmony import */ var _node_modules_pnpm_css_loader_7_1_2_webpack_5_104_1_node_modules_css_loader_dist_runtime_api_js__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(/*! ../../node_modules/.pnpm/css-loader@7.1.2_webpack@5.104.1/node_modules/css-loader/dist/runtime/api.js */ "./node_modules/.pnpm/css-loader@7.1.2_webpack@5.104.1/node_modules/css-loader/dist/runtime/api.js");
72
+ /* harmony import */ var _node_modules_pnpm_css_loader_7_1_2_webpack_5_104_1_node_modules_css_loader_dist_runtime_api_js__WEBPACK_IMPORTED_MODULE_1___default = /*#__PURE__*/__webpack_require__.n(_node_modules_pnpm_css_loader_7_1_2_webpack_5_104_1_node_modules_css_loader_dist_runtime_api_js__WEBPACK_IMPORTED_MODULE_1__);
73
+ // Imports
74
+
75
+
76
+ var ___CSS_LOADER_EXPORT___ = _node_modules_pnpm_css_loader_7_1_2_webpack_5_104_1_node_modules_css_loader_dist_runtime_api_js__WEBPACK_IMPORTED_MODULE_1___default()((_node_modules_pnpm_css_loader_7_1_2_webpack_5_104_1_node_modules_css_loader_dist_runtime_sourceMaps_js__WEBPACK_IMPORTED_MODULE_0___default()));
77
+ // Module
78
+ ___CSS_LOADER_EXPORT___.push([module.id, `@charset "UTF-8";
79
+ /*
80
+ * TmuxPaneTab — full CSS control over the terminal content area.
81
+ *
82
+ * BaseTerminalTabComponent applies a "spaciness" margin that shrinks the
83
+ * content box, which clips edge characters and makes size calculations
84
+ * harder. tmux owns the cell grid, so we zero out every spacing property
85
+ * so the pane occupies 100 % of its allocated container — pixel-accurate
86
+ * and no rounding surprises.
87
+ *
88
+ * Uniform 4px padding on all sides gives consistent visual breathing room.
89
+ * The scrollbar is hidden to remove the asymmetric right-side gap and
90
+ * simplify size math; scrolling is handled by Tabby's local history buffer.
91
+ * Padding is subtracted in measureClientSize() so tmux grid calculations
92
+ * remain accurate.
93
+ */
94
+ :host > .content {
95
+ margin: 0;
96
+ padding: 4px;
97
+ border: 0;
98
+ box-sizing: border-box;
99
+ }
100
+
101
+ /* Hide xterm scrollbar — tmux handles scrolling via Control Mode protocol */
102
+ :host ::ng-deep .xterm {
103
+ overflow: hidden !important;
104
+ }
105
+
106
+ :host ::ng-deep .xterm-viewport {
107
+ overflow-y: hidden !important;
108
+ }`, "",{"version":3,"sources":["webpack://./src/components/tmuxPaneTab.component.scss"],"names":[],"mappings":"AAAA,gBAAgB;AAAhB;;;;;;;;;;;;;;EAAA;AAeA;EACI,SAAA;EACA,YAAA;EACA,SAAA;EACA,sBAAA;AAEJ;;AACA,4EAAA;AACA;EACI,2BAAA;AAEJ;;AAAA;EACI,6BAAA;AAGJ","sourcesContent":["/*\n * TmuxPaneTab — full CSS control over the terminal content area.\n *\n * BaseTerminalTabComponent applies a \"spaciness\" margin that shrinks the\n * content box, which clips edge characters and makes size calculations\n * harder. tmux owns the cell grid, so we zero out every spacing property\n * so the pane occupies 100 % of its allocated container — pixel-accurate\n * and no rounding surprises.\n *\n * Uniform 4px padding on all sides gives consistent visual breathing room.\n * The scrollbar is hidden to remove the asymmetric right-side gap and\n * simplify size math; scrolling is handled by Tabby's local history buffer.\n * Padding is subtracted in measureClientSize() so tmux grid calculations\n * remain accurate.\n */\n:host > .content {\n margin: 0;\n padding: 4px;\n border: 0;\n box-sizing: border-box;\n}\n\n/* Hide xterm scrollbar — tmux handles scrolling via Control Mode protocol */\n:host ::ng-deep .xterm {\n overflow: hidden !important;\n}\n:host ::ng-deep .xterm-viewport {\n overflow-y: hidden !important;\n}\n"],"sourceRoot":""}]);
109
+ // Exports
110
+ /* harmony default export */ const __WEBPACK_DEFAULT_EXPORT__ = (___CSS_LOADER_EXPORT___);
111
+
112
+
113
+ /***/ },
114
+
115
+ /***/ "./node_modules/.pnpm/css-loader@7.1.2_webpack@5.104.1/node_modules/css-loader/dist/runtime/api.js"
116
+ /*!*********************************************************************************************************!*\
117
+ !*** ./node_modules/.pnpm/css-loader@7.1.2_webpack@5.104.1/node_modules/css-loader/dist/runtime/api.js ***!
118
+ \*********************************************************************************************************/
119
+ (module) {
120
+
121
+ "use strict";
122
+
123
+
124
+ /*
125
+ MIT License http://www.opensource.org/licenses/mit-license.php
126
+ Author Tobias Koppers @sokra
127
+ */
128
+ module.exports = function (cssWithMappingToString) {
129
+ var list = [];
130
+
131
+ // return the list of modules as css string
132
+ list.toString = function toString() {
133
+ return this.map(function (item) {
134
+ var content = "";
135
+ var needLayer = typeof item[5] !== "undefined";
136
+ if (item[4]) {
137
+ content += "@supports (".concat(item[4], ") {");
138
+ }
139
+ if (item[2]) {
140
+ content += "@media ".concat(item[2], " {");
141
+ }
142
+ if (needLayer) {
143
+ content += "@layer".concat(item[5].length > 0 ? " ".concat(item[5]) : "", " {");
144
+ }
145
+ content += cssWithMappingToString(item);
146
+ if (needLayer) {
147
+ content += "}";
148
+ }
149
+ if (item[2]) {
150
+ content += "}";
151
+ }
152
+ if (item[4]) {
153
+ content += "}";
154
+ }
155
+ return content;
156
+ }).join("");
157
+ };
158
+
159
+ // import a list of modules into the list
160
+ list.i = function i(modules, media, dedupe, supports, layer) {
161
+ if (typeof modules === "string") {
162
+ modules = [[null, modules, undefined]];
163
+ }
164
+ var alreadyImportedModules = {};
165
+ if (dedupe) {
166
+ for (var k = 0; k < this.length; k++) {
167
+ var id = this[k][0];
168
+ if (id != null) {
169
+ alreadyImportedModules[id] = true;
170
+ }
171
+ }
172
+ }
173
+ for (var _k = 0; _k < modules.length; _k++) {
174
+ var item = [].concat(modules[_k]);
175
+ if (dedupe && alreadyImportedModules[item[0]]) {
176
+ continue;
177
+ }
178
+ if (typeof layer !== "undefined") {
179
+ if (typeof item[5] === "undefined") {
180
+ item[5] = layer;
181
+ } else {
182
+ item[1] = "@layer".concat(item[5].length > 0 ? " ".concat(item[5]) : "", " {").concat(item[1], "}");
183
+ item[5] = layer;
184
+ }
185
+ }
186
+ if (media) {
187
+ if (!item[2]) {
188
+ item[2] = media;
189
+ } else {
190
+ item[1] = "@media ".concat(item[2], " {").concat(item[1], "}");
191
+ item[2] = media;
192
+ }
193
+ }
194
+ if (supports) {
195
+ if (!item[4]) {
196
+ item[4] = "".concat(supports);
197
+ } else {
198
+ item[1] = "@supports (".concat(item[4], ") {").concat(item[1], "}");
199
+ item[4] = supports;
200
+ }
201
+ }
202
+ list.push(item);
203
+ }
204
+ };
205
+ return list;
206
+ };
207
+
208
+ /***/ },
209
+
210
+ /***/ "./node_modules/.pnpm/css-loader@7.1.2_webpack@5.104.1/node_modules/css-loader/dist/runtime/sourceMaps.js"
211
+ /*!****************************************************************************************************************!*\
212
+ !*** ./node_modules/.pnpm/css-loader@7.1.2_webpack@5.104.1/node_modules/css-loader/dist/runtime/sourceMaps.js ***!
213
+ \****************************************************************************************************************/
214
+ (module) {
215
+
216
+ "use strict";
217
+
218
+
219
+ module.exports = function (item) {
220
+ var content = item[1];
221
+ var cssMapping = item[3];
222
+ if (!cssMapping) {
223
+ return content;
224
+ }
225
+ if (typeof btoa === "function") {
226
+ var base64 = btoa(unescape(encodeURIComponent(JSON.stringify(cssMapping))));
227
+ var data = "sourceMappingURL=data:application/json;charset=utf-8;base64,".concat(base64);
228
+ var sourceMapping = "/*# ".concat(data, " */");
229
+ return [content].concat([sourceMapping]).join("\n");
230
+ }
231
+ return [content].join("\n");
232
+ };
233
+
234
+ /***/ },
235
+
236
+ /***/ "./src/components/settings.component.scss"
237
+ /*!************************************************!*\
238
+ !*** ./src/components/settings.component.scss ***!
239
+ \************************************************/
240
+ (module, __unused_webpack_exports, __webpack_require__) {
241
+
242
+
243
+ var result = __webpack_require__(/*! !!../../node_modules/.pnpm/css-loader@7.1.2_webpack@5.104.1/node_modules/css-loader/dist/cjs.js!../../node_modules/.pnpm/sass-loader@16.0.6_sass@1.97.2_webpack@5.104.1/node_modules/sass-loader/dist/cjs.js!./settings.component.scss */ "./node_modules/.pnpm/css-loader@7.1.2_webpack@5.104.1/node_modules/css-loader/dist/cjs.js!./node_modules/.pnpm/sass-loader@16.0.6_sass@1.97.2_webpack@5.104.1/node_modules/sass-loader/dist/cjs.js!./src/components/settings.component.scss");
244
+
245
+ if (result && result.__esModule) {
246
+ result = result.default;
247
+ }
248
+
249
+ if (typeof result === "string") {
250
+ module.exports = result;
251
+ } else {
252
+ module.exports = result.toString();
253
+ }
254
+
255
+
256
+ /***/ },
257
+
258
+ /***/ "./src/components/settings.component.ts"
259
+ /*!**********************************************!*\
260
+ !*** ./src/components/settings.component.ts ***!
261
+ \**********************************************/
262
+ (__unused_webpack_module, exports, __webpack_require__) {
263
+
264
+ "use strict";
265
+
266
+ var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
267
+ var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
268
+ if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
269
+ else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
270
+ return c > 3 && r && Object.defineProperty(target, key, r), r;
271
+ };
272
+ var __metadata = (this && this.__metadata) || function (k, v) {
273
+ if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
274
+ };
275
+ Object.defineProperty(exports, "__esModule", ({ value: true }));
276
+ exports.TmuxSettingsTabComponent = void 0;
277
+ const core_1 = __webpack_require__(/*! @angular/core */ "@angular/core");
278
+ const tabby_core_1 = __webpack_require__(/*! tabby-core */ "tabby-core");
279
+ // eslint-disable-next-line new-cap
280
+ let TmuxSettingsTabComponent = class TmuxSettingsTabComponent {
281
+ constructor(config) {
282
+ this.config = config;
283
+ }
284
+ };
285
+ TmuxSettingsTabComponent.ctorParameters = () => [
286
+ { type: tabby_core_1.ConfigService }
287
+ ];
288
+ TmuxSettingsTabComponent = __decorate([
289
+ (0, core_1.Component)({
290
+ template: `
291
+ <h3>Tmux</h3>
292
+ <div class="tmux-settings-tab">
293
+ <div class="tmux-table">
294
+ <div class="row">
295
+ <div class="header"><div class="title">Default session name:</div></div>
296
+ <input class="form-control" type="text"
297
+ [(ngModel)]="config.store.tmuxPlugin.defaultSessionName"
298
+ (ngModelChange)="config.save()">
299
+ </div>
300
+ <div class="row">
301
+ <div class="header"><div class="title">Command timeout (ms):</div></div>
302
+ <input class="form-control" type="number"
303
+ [(ngModel)]="config.store.tmuxPlugin.commandTimeoutMs"
304
+ (ngModelChange)="config.save()">
305
+ </div>
306
+ <div class="row">
307
+ <div class="header"><div class="title">Send-keys chunk size:</div></div>
308
+ <input class="form-control" type="number"
309
+ [(ngModel)]="config.store.tmuxPlugin.sendKeysChunkSize"
310
+ (ngModelChange)="config.save()">
311
+ </div>
312
+ <div class="row">
313
+ <div class="header"><div class="title">Resize debounce (ms):</div></div>
314
+ <input class="form-control" type="number"
315
+ [(ngModel)]="config.store.tmuxPlugin.resizeDebounceMs"
316
+ (ngModelChange)="config.save()">
317
+ </div>
318
+ <div class="row">
319
+ <div class="header"><div class="title">Debug logging:</div></div>
320
+ <input type="checkbox"
321
+ [(ngModel)]="config.store.tmuxPlugin.debugLogging"
322
+ (ngModelChange)="config.save()">
323
+ </div>
324
+ </div>
325
+ </div>
326
+ `,
327
+ styles: [__webpack_require__(/*! ./settings.component.scss */ "./src/components/settings.component.scss")]
328
+ }),
329
+ __metadata("design:paramtypes", [tabby_core_1.ConfigService])
330
+ ], TmuxSettingsTabComponent);
331
+ exports.TmuxSettingsTabComponent = TmuxSettingsTabComponent;
332
+
333
+
334
+ /***/ },
335
+
336
+ /***/ "./src/components/tmuxPaneTab.component.scss"
337
+ /*!***************************************************!*\
338
+ !*** ./src/components/tmuxPaneTab.component.scss ***!
339
+ \***************************************************/
340
+ (module, __unused_webpack_exports, __webpack_require__) {
341
+
342
+
343
+ var result = __webpack_require__(/*! !!../../node_modules/.pnpm/css-loader@7.1.2_webpack@5.104.1/node_modules/css-loader/dist/cjs.js!../../node_modules/.pnpm/sass-loader@16.0.6_sass@1.97.2_webpack@5.104.1/node_modules/sass-loader/dist/cjs.js!./tmuxPaneTab.component.scss */ "./node_modules/.pnpm/css-loader@7.1.2_webpack@5.104.1/node_modules/css-loader/dist/cjs.js!./node_modules/.pnpm/sass-loader@16.0.6_sass@1.97.2_webpack@5.104.1/node_modules/sass-loader/dist/cjs.js!./src/components/tmuxPaneTab.component.scss");
344
+
345
+ if (result && result.__esModule) {
346
+ result = result.default;
347
+ }
348
+
349
+ if (typeof result === "string") {
350
+ module.exports = result;
351
+ } else {
352
+ module.exports = result.toString();
353
+ }
354
+
355
+
356
+ /***/ },
357
+
358
+ /***/ "./src/components/tmuxPaneTab.component.ts"
359
+ /*!*************************************************!*\
360
+ !*** ./src/components/tmuxPaneTab.component.ts ***!
361
+ \*************************************************/
362
+ (__unused_webpack_module, exports, __webpack_require__) {
363
+
364
+ "use strict";
365
+
366
+ var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
367
+ var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
368
+ if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
369
+ else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
370
+ return c > 3 && r && Object.defineProperty(target, key, r), r;
371
+ };
372
+ var __metadata = (this && this.__metadata) || function (k, v) {
373
+ if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
374
+ };
375
+ Object.defineProperty(exports, "__esModule", ({ value: true }));
376
+ exports.TmuxPaneTabComponent = void 0;
377
+ const core_1 = __webpack_require__(/*! @angular/core */ "@angular/core");
378
+ const rxjs_1 = __webpack_require__(/*! rxjs */ "rxjs");
379
+ const tabby_terminal_1 = __webpack_require__(/*! tabby-terminal */ "tabby-terminal");
380
+ const session_1 = __webpack_require__(/*! ../session */ "./src/session.ts");
381
+ let TmuxPaneTabComponent = class TmuxPaneTabComponent extends tabby_terminal_1.BaseTerminalTabComponent {
382
+ constructor(injector) {
383
+ super(injector);
384
+ /**
385
+ * Whether this pane is the active (keyboard-focused) pane in the tmux session.
386
+ * Controls whether hotkey-triggered input (e.g. Ctrl+C, paste) is forwarded.
387
+ *
388
+ * All tmux pane tabs have `hasFocus = true` simultaneously (needed for
389
+ * xterm frontend initialization), but only one pane should process hotkeys.
390
+ * This flag is managed by TmuxSessionTabComponent.focus().
391
+ */
392
+ this._tmuxActive = true;
393
+ /**
394
+ * When true, input is broadcast to all panes in the tmux session
395
+ * ("Focus all tmux panes" / synchronize-panes mode).
396
+ * Toggled via the right-click context menu.
397
+ */
398
+ this._tmuxSyncInput = false;
399
+ /** Desired tmux grid size (chars). tmux is authoritative over the cell grid. */
400
+ this._tmuxCols = 0;
401
+ this._tmuxRows = 0;
402
+ /** Whether the xterm frontend has been attached and is ready. */
403
+ this._frontendReady = false;
404
+ }
405
+ ngOnInit() {
406
+ // Profile must be set BEFORE calling super.ngOnInit() because
407
+ // the parent class configures the terminal frontend using profile settings
408
+ this.profile = {
409
+ name: `Tmux Pane %${this.paneId}`,
410
+ type: 'tmux',
411
+ options: {},
412
+ // Required properties for BaseTerminalTabComponent
413
+ behaviorOnSessionEnd: 'close',
414
+ terminalColorScheme: null, // Use default
415
+ };
416
+ this.setTitle(`Pane %${this.paneId}`);
417
+ // Now call parent's ngOnInit to set up the frontend.
418
+ // NOTE: super.ngOnInit() schedules a setImmediate that checks
419
+ // this.hasFocus to decide whether to attach the xterm frontend.
420
+ // BaseTabComponent sets hasFocus=true on focused$.next().
421
+ // We emit focus synchronously right after super.ngOnInit() so that
422
+ // when setImmediate fires, hasFocus is already true.
423
+ super.ngOnInit();
424
+ // Mark this tab as focused so the setImmediate in super.ngOnInit
425
+ // will attach the frontend to the DOM element.
426
+ // This is safe because our overridden focus() doesn't blur siblings.
427
+ this.emitFocused();
428
+ // Initialize our session AFTER emitting focus, so that:
429
+ // 1. frontend.attach() runs via setImmediate (because hasFocus=true)
430
+ // 2. frontend.resize$ fires, which triggers releaseInitialDataBuffer()
431
+ // 3. Then session.start() populates the buffer and it gets released
432
+ //
433
+ // The key insight: setImmediate fires before our async session.start(),
434
+ // so the frontend is attached before history restore begins. This means
435
+ // history output goes directly to the terminal, not into a buffer that
436
+ // gets flushed in a bulk dump.
437
+ this.initializeSession();
438
+ // tmux owns the cell grid. Once the frontend is ready, neutralize
439
+ // xterm's automatic fit-to-container so the pane never overrides the
440
+ // tmux-dictated grid with its own (pixel-rounded) size — that mismatch
441
+ // is what causes off-by-one wrapping / cursor errors. The grid is set
442
+ // explicitly via setTmuxGrid() from the layout sync instead.
443
+ this.frontendReady$.pipe((0, rxjs_1.first)()).subscribe(() => {
444
+ this._frontendReady = true;
445
+ const frontend = this.frontend;
446
+ if (frontend) {
447
+ frontend.enableResizing = false;
448
+ // The frontend's resizeHandler (window resize + ResizeObserver)
449
+ // calls fitAddon.fit() unconditionally and ignores enableResizing.
450
+ // Replace fit() with a no-op so the grid stays exactly what tmux
451
+ // tells us. Keep a reference in case we ever need to restore it.
452
+ if (frontend.fitAddon && typeof frontend.fitAddon.fit === 'function') {
453
+ frontend.fitAddon.fit = () => { };
454
+ }
455
+ }
456
+ // Apply any grid size that arrived before the frontend was ready.
457
+ if (this._tmuxCols > 0 && this._tmuxRows > 0) {
458
+ this.applyTmuxGrid();
459
+ }
460
+ });
461
+ }
462
+ /**
463
+ * Set the authoritative cell grid for this pane, as dictated by the tmux
464
+ * layout string. tmux decides each pane's exact character width/height, so
465
+ * we resize the xterm grid to match instead of letting xterm fit to pixels.
466
+ * This keeps wrapping aligned with tmux and removes the resize feedback loop.
467
+ */
468
+ setTmuxGrid(cols, rows) {
469
+ if (cols <= 0 || rows <= 0)
470
+ return;
471
+ if (cols === this._tmuxCols && rows === this._tmuxRows)
472
+ return;
473
+ this._tmuxCols = cols;
474
+ this._tmuxRows = rows;
475
+ if (this._frontendReady) {
476
+ this.applyTmuxGrid();
477
+ }
478
+ }
479
+ applyTmuxGrid() {
480
+ var _a;
481
+ const xterm = (_a = this.frontend) === null || _a === void 0 ? void 0 : _a.xterm;
482
+ if (!xterm)
483
+ return;
484
+ if (xterm.cols === this._tmuxCols && xterm.rows === this._tmuxRows)
485
+ return;
486
+ try {
487
+ xterm.resize(this._tmuxCols, this._tmuxRows);
488
+ }
489
+ catch (e) {
490
+ this.logger.warn(`Failed to resize pane %${this.paneId} grid`, e);
491
+ }
492
+ // xterm.resize() clears the alternate screen buffer.
493
+ // Re-apply saved alternate content if this pane was on it.
494
+ const session = this.session;
495
+ if ((session === null || session === void 0 ? void 0 : session.pendingAltRestore) && this.controller) {
496
+ this.controller.reapplyAltContent(session);
497
+ }
498
+ }
499
+ async initializeSession() {
500
+ if (!this.controller) {
501
+ throw new Error('Tmux controller not provided to pane tab');
502
+ }
503
+ // Create the pane session
504
+ const paneSession = new session_1.TmuxPaneSession(this.logger, this.controller, this.paneId);
505
+ // Set up the terminal session first so the frontend is wired.
506
+ // This binds session.output$ → this.write() and frontend → session.
507
+ this.setSession(paneSession, true);
508
+ // Start the session (restores history) non-blocking.
509
+ // History is written to the terminal via emitOutput → write().
510
+ paneSession.start();
511
+ }
512
+ /**
513
+ * Guard sendInput so that only the active pane forwards hotkey-triggered
514
+ * input (Ctrl+C, Home, End, etc.) to its tmux session.
515
+ *
516
+ * When _tmuxSyncInput is enabled ("Focus all tmux panes"), input is also
517
+ * broadcast to all other panes in the session.
518
+ */
519
+ sendInput(data) {
520
+ if (!this._tmuxActive) {
521
+ return;
522
+ }
523
+ super.sendInput(data);
524
+ // Broadcast to all other panes when sync mode is active
525
+ if (this._tmuxSyncInput && this.controller) {
526
+ const buf = Buffer.isBuffer(data) ? data : Buffer.from(data);
527
+ for (const pid of this.controller.getAllPaneIds()) {
528
+ if (pid !== this.paneId) {
529
+ this.controller.writeToPane(pid, buf);
530
+ }
531
+ }
532
+ }
533
+ }
534
+ /**
535
+ * Guard paste so that only the active pane pastes into its tmux session.
536
+ */
537
+ async paste() {
538
+ if (!this._tmuxActive) {
539
+ return;
540
+ }
541
+ return super.paste();
542
+ }
543
+ /**
544
+ * Always allow closing a tmux pane tab without showing the
545
+ * "command is still running" confirmation dialog.
546
+ * The tmux server process is not a user command — lifetime is
547
+ * managed separately by TmuxService/TmuxController.
548
+ */
549
+ async canClose() {
550
+ return true;
551
+ }
552
+ // Override generic title behavior
553
+ getCustomTitle() {
554
+ return `Tmux Pane %${this.paneId}`;
555
+ }
556
+ /**
557
+ * Override the native context menu to provide tmux-specific items only.
558
+ * Keeps: Copy, Paste, Close (pane).
559
+ * Adds: Exit Tmux Mode, Split submenu, Focus all tmux panes.
560
+ */
561
+ async buildContextMenu() {
562
+ const items = [
563
+ {
564
+ label: this.translate.instant('Copy'),
565
+ click: () => { var _a; return (_a = this.frontend) === null || _a === void 0 ? void 0 : _a.copySelection(); },
566
+ },
567
+ {
568
+ label: this.translate.instant('Paste'),
569
+ click: () => this.paste(),
570
+ },
571
+ { type: 'separator' },
572
+ {
573
+ label: this.translate.instant('Split'),
574
+ submenu: [
575
+ { label: this.translate.instant('Right'), click: () => this.splitPane('right') },
576
+ { label: this.translate.instant('Down'), click: () => this.splitPane('down') },
577
+ { label: this.translate.instant('Left'), click: () => this.splitPane('left') },
578
+ { label: this.translate.instant('Up'), click: () => this.splitPane('up') },
579
+ ],
580
+ },
581
+ {
582
+ label: this.translate.instant('Focus all tmux panes'),
583
+ type: 'checkbox',
584
+ checked: this._tmuxSyncInput,
585
+ click: () => this.toggleSyncInput(),
586
+ },
587
+ { type: 'separator' },
588
+ {
589
+ label: this.translate.instant('Close'),
590
+ click: () => this.closePane(),
591
+ },
592
+ ];
593
+ return items;
594
+ }
595
+ async handleRightMouseDown(event) {
596
+ event.preventDefault();
597
+ event.stopPropagation();
598
+ this.platform.popupContextMenu(await this.buildContextMenu(), event);
599
+ }
600
+ async splitPane(direction) {
601
+ if (!this.controller)
602
+ return;
603
+ const flagMap = {
604
+ 'right': '-h',
605
+ 'down': '-v',
606
+ 'left': '-h -b',
607
+ 'up': '-v -b',
608
+ };
609
+ await this.controller.gateway.sendCommand(`split-window ${flagMap[direction]} -t %${this.paneId}`);
610
+ // No explicit refresh needed — the %layout-change notification
611
+ // from tmux will trigger discoverPanesFromLayout() in TmuxController,
612
+ // which discovers the new pane and emits pane-add with pre-loaded
613
+ // history (iTerm2-style).
614
+ }
615
+ async closePane() {
616
+ if (!this.controller)
617
+ return;
618
+ await this.controller.killPane(this.paneId);
619
+ }
620
+ /**
621
+ * Toggle "Focus all tmux panes" (synchronize input) across all panes
622
+ * in the current tmux session.
623
+ */
624
+ toggleSyncInput() {
625
+ if (!this.controller)
626
+ return;
627
+ const newValue = !this._tmuxSyncInput;
628
+ for (const pid of this.controller.getAllPaneIds()) {
629
+ const tab = this.findPaneTab(pid);
630
+ if (tab) {
631
+ tab._tmuxSyncInput = newValue;
632
+ }
633
+ }
634
+ }
635
+ findPaneTab(paneId) {
636
+ // Walk the session tab's window pane map to find the tab
637
+ const parent = this.parent;
638
+ if (parent === null || parent === void 0 ? void 0 : parent.windowPaneTabs) {
639
+ for (const paneMap of parent.windowPaneTabs.values()) {
640
+ const tab = paneMap.get(paneId);
641
+ if (tab)
642
+ return tab;
643
+ }
644
+ }
645
+ return null;
646
+ }
647
+ };
648
+ TmuxPaneTabComponent.ctorParameters = () => [
649
+ { type: core_1.Injector }
650
+ ];
651
+ TmuxPaneTabComponent.propDecorators = {
652
+ controller: [{ type: core_1.Input }],
653
+ paneId: [{ type: core_1.Input }]
654
+ };
655
+ TmuxPaneTabComponent = __decorate([
656
+ (0, core_1.Component)({
657
+ selector: 'tmux-pane-tab',
658
+ template: tabby_terminal_1.BaseTerminalTabComponent.template,
659
+ animations: tabby_terminal_1.BaseTerminalTabComponent.animations,
660
+ styles: [...tabby_terminal_1.BaseTerminalTabComponent.styles,
661
+ __webpack_require__(/*! ./tmuxPaneTab.component.scss */ "./src/components/tmuxPaneTab.component.scss")]
662
+ }),
663
+ __metadata("design:paramtypes", [core_1.Injector])
664
+ ], TmuxPaneTabComponent);
665
+ exports.TmuxPaneTabComponent = TmuxPaneTabComponent;
666
+
667
+
668
+ /***/ },
669
+
670
+ /***/ "./src/components/tmuxSessionTab.component.ts"
671
+ /*!****************************************************!*\
672
+ !*** ./src/components/tmuxSessionTab.component.ts ***!
673
+ \****************************************************/
674
+ (__unused_webpack_module, exports, __webpack_require__) {
675
+
676
+ "use strict";
677
+
678
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
679
+ if (k2 === undefined) k2 = k;
680
+ var desc = Object.getOwnPropertyDescriptor(m, k);
681
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
682
+ desc = { enumerable: true, get: function() { return m[k]; } };
683
+ }
684
+ Object.defineProperty(o, k2, desc);
685
+ }) : (function(o, m, k, k2) {
686
+ if (k2 === undefined) k2 = k;
687
+ o[k2] = m[k];
688
+ }));
689
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
690
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
691
+ }) : function(o, v) {
692
+ o["default"] = v;
693
+ });
694
+ var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
695
+ var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
696
+ if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
697
+ else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
698
+ return c > 3 && r && Object.defineProperty(target, key, r), r;
699
+ };
700
+ var __importStar = (this && this.__importStar) || function (mod) {
701
+ if (mod && mod.__esModule) return mod;
702
+ var result = {};
703
+ if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
704
+ __setModuleDefault(result, mod);
705
+ return result;
706
+ };
707
+ var __metadata = (this && this.__metadata) || function (k, v) {
708
+ if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
709
+ };
710
+ var TmuxSessionTabComponent_1;
711
+ Object.defineProperty(exports, "__esModule", ({ value: true }));
712
+ exports.TmuxSessionTabComponent = void 0;
713
+ const core_1 = __webpack_require__(/*! @angular/core */ "@angular/core");
714
+ const tabby_core_1 = __webpack_require__(/*! tabby-core */ "tabby-core");
715
+ const tabby_core_2 = __webpack_require__(/*! tabby-core */ "tabby-core");
716
+ const session_1 = __webpack_require__(/*! ../session */ "./src/session.ts");
717
+ const tmux_service_1 = __webpack_require__(/*! ../services/tmux.service */ "./src/services/tmux.service.ts");
718
+ const tmuxPaneTab_component_1 = __webpack_require__(/*! ./tmuxPaneTab.component */ "./src/components/tmuxPaneTab.component.ts");
719
+ const layoutParser_1 = __webpack_require__(/*! ../layoutParser */ "./src/layoutParser.ts");
720
+ /**
721
+ * TmuxSessionTabComponent - Manages an entire tmux session within a single Tabby tab.
722
+ *
723
+ * Each tmux window is represented by its pane tabs, which are hidden/shown via
724
+ * removeTab()/addTab() when switching windows. The bottom window bar provides
725
+ * window switching UI.
726
+ *
727
+ * Always created by TmuxService.attachToTerminal() with existingController set.
728
+ */
729
+ let TmuxSessionTabComponent = TmuxSessionTabComponent_1 = class TmuxSessionTabComponent extends tabby_core_1.SplitTabComponent {
730
+ constructor(injector, tmuxService, configService, tabsService, cdr, hostElement, log) {
731
+ super(injector.get(tabby_core_1.HotkeysService), tabsService, injector.get(tabby_core_2.TabRecoveryService), injector);
732
+ this.tmuxService = tmuxService;
733
+ this.configService = configService;
734
+ this.cdr = cdr;
735
+ this.hostElement = hostElement;
736
+ this.profile = {};
737
+ this.eventSubscription = null;
738
+ // windowId → (paneId → paneTab)
739
+ this.windowPaneTabs = new Map();
740
+ /** Queue for serializing async event processing */
741
+ this.eventQueue = Promise.resolve();
742
+ this.controller = null;
743
+ this.activeWindowId = null;
744
+ this.connected = false;
745
+ this.sessionName = '';
746
+ this._initialized = false;
747
+ this._resizeHandler = null;
748
+ this._resizeTimer = null;
749
+ this._paneAreaObserver = null;
750
+ /** Last dimensions sent to tmux, for dedup */
751
+ this._lastSentCols = 0;
752
+ this._lastSentRows = 0;
753
+ /** Custom tmux pane dividers — kept for interface compatibility but not rendered */
754
+ this._tmuxDividers = [];
755
+ /** mousedown handler attached to .pane-area for border drag detection */
756
+ this._paneAreaMouseDownHandler = null;
757
+ /** mousemove handler for border hover highlight */
758
+ this._paneAreaMouseMoveHandler = null;
759
+ this._tabsService = tabsService;
760
+ this.logger = log.create('tmux-session');
761
+ }
762
+ ngOnInit() {
763
+ this.logger.info('ngOnInit initialized');
764
+ this.controller = this.existingController;
765
+ if (!this.controller) {
766
+ this.logger.error('No controller provided');
767
+ return;
768
+ }
769
+ this.sessionName = this.controller.getSessionName() || this.profile.sessionName || 'default';
770
+ this.setTitle(`Tmux: ${this.sessionName}`);
771
+ // Subscribe to controller events.
772
+ // Events are queued to ensure serial async processing — critical because
773
+ // handleControllerEvent contains async operations (switchToWindow, syncLayout)
774
+ // that must not interleave. Without serialization, concurrent switches from
775
+ // multiple window-add events (during refreshPanes) corrupt activeWindowId.
776
+ this.eventSubscription = this.controller.events.subscribe(event => {
777
+ this.eventQueue = this.eventQueue.then(() => this.handleControllerEvent(event));
778
+ });
779
+ // Bootstrap from current controller snapshot in case early events were missed
780
+ this.bootstrapFromControllerState();
781
+ }
782
+ /**
783
+ * Called after the view is initialized.
784
+ * The parent SplitTabComponent has finished its own ngAfterViewInit
785
+ * (including recoverContainer if any), so #vc is ready.
786
+ */
787
+ async ngAfterViewInit() {
788
+ await super.ngAfterViewInit();
789
+ if (!this.controller)
790
+ return;
791
+ // Wait one more frame to ensure the wrapper's attachTabView
792
+ // has finished inserting us into its ViewContainerRef
793
+ requestAnimationFrame(async () => {
794
+ this._initialized = true;
795
+ await this.controller.refreshPanes();
796
+ this.bootstrapFromControllerState();
797
+ // Wait for all queued events (window-add, pane-add) from refreshPanes
798
+ // to be processed before switching to the first window.
799
+ await this.eventQueue;
800
+ // Prefer the tmux-side active window (from list-windows #{window_active}
801
+ // or %session-window-changed). Fall back to the first window in the map.
802
+ const activeWindowId = this.controller.getActiveWindowId();
803
+ const targetWindowId = (activeWindowId !== null && this.windowPaneTabs.has(activeWindowId))
804
+ ? activeWindowId
805
+ : this.controller.getFirstWindowId();
806
+ if (targetWindowId !== undefined) {
807
+ await this.switchToWindow(targetWindowId);
808
+ }
809
+ // Listen for window resize events (like iTerm2's windowDidResize).
810
+ // Only fires when the browser window changes size, not during
811
+ // internal SplitTab layout operations. Debounced to avoid flooding.
812
+ this._resizeHandler = () => this.scheduleRefreshClientSize();
813
+ window.addEventListener('resize', this._resizeHandler);
814
+ // Observe the .pane-area container directly. This is the single
815
+ // source of truth for the client size: any time the container's
816
+ // pixel size changes (window resize, spanner drag, sidebar toggle,
817
+ // first mount), we recompute and push the whole-window grid to tmux.
818
+ // Per-pane xterm fit is disabled, so this never feeds back.
819
+ const host = this.hostElement.nativeElement;
820
+ const paneArea = host.querySelector('.pane-area');
821
+ if (paneArea && typeof ResizeObserver !== 'undefined') {
822
+ this._paneAreaObserver = new ResizeObserver(() => this.scheduleRefreshClientSize());
823
+ this._paneAreaObserver.observe(paneArea);
824
+ }
825
+ // Attach border hover + drag handlers to the pane-area
826
+ this.attachPaneAreaBorderHandlers();
827
+ // Initial size sync after pane mount
828
+ this.scheduleRefreshClientSize();
829
+ });
830
+ }
831
+ bootstrapFromControllerState() {
832
+ if (!this.controller) {
833
+ return;
834
+ }
835
+ // Prime local maps from controller state so UI can render even if
836
+ // window-add / pane-add events happened before this component subscribed.
837
+ // This is critical: discoverWindowsAndPanes() emits window-add and pane-add
838
+ // during the first call (triggered by session-changed), but the SessionTab
839
+ // component may not exist yet. By the time ngOnInit runs, the controller
840
+ // already knows about all windows and panes — we must create pane tabs
841
+ // here so switchToWindow finds non-empty paneMaps.
842
+ for (const windowState of this.controller.getAllWindowStates()) {
843
+ if (!this.windowPaneTabs.has(windowState.id)) {
844
+ this.windowPaneTabs.set(windowState.id, new Map());
845
+ }
846
+ // Create pane tabs for all panes the controller already knows about
847
+ const paneMap = this.windowPaneTabs.get(windowState.id);
848
+ const ctrlWindowState = this.controller.getWindowState(windowState.id);
849
+ if (ctrlWindowState) {
850
+ for (const paneId of ctrlWindowState.panes) {
851
+ if (!paneMap.has(paneId)) {
852
+ this.logger.info(`Bootstrap: creating pane tab for %${paneId} in window @${windowState.id}`);
853
+ const paneTab = this.createPaneTab(paneId);
854
+ paneTab.controller = this.controller;
855
+ paneTab.paneId = paneId;
856
+ paneMap.set(paneId, paneTab);
857
+ }
858
+ }
859
+ }
860
+ }
861
+ if (this.controller.isAttached) {
862
+ this.connected = true;
863
+ }
864
+ this.cdr.detectChanges();
865
+ }
866
+ async handleControllerEvent(event) {
867
+ var _a, _b;
868
+ this.logger.info('SessionTab event:', event.type, event);
869
+ switch (event.type) {
870
+ case 'initialized':
871
+ case 'session-changed':
872
+ this.connected = true;
873
+ this.sessionName = ((_a = this.controller) === null || _a === void 0 ? void 0 : _a.getSessionName()) || this.profile.sessionName || 'default';
874
+ this.setTitle(`Tmux: ${this.sessionName}`);
875
+ this.cdr.detectChanges();
876
+ break;
877
+ case 'window-add':
878
+ if (event.windowId !== undefined) {
879
+ const isNewWindow = !this.windowPaneTabs.has(event.windowId);
880
+ // Ensure the window has an entry in our map
881
+ if (isNewWindow) {
882
+ this.logger.info(`Adding new window @${event.windowId} to map`);
883
+ this.windowPaneTabs.set(event.windowId, new Map());
884
+ }
885
+ // Switch to a new window if:
886
+ // 1. No active window yet (initial attach), OR
887
+ // 2. This is a genuinely new window created after attach
888
+ // AND we are past the initial bootstrap phase.
889
+ // During batch discovery, we defer switching to ngAfterViewInit.
890
+ //
891
+ // MUST await: switchToWindow is async and rebuilds the SplitContainer
892
+ // tree. Without await, concurrent switches from multiple window-add
893
+ // events (during refreshPanes) interleave and corrupt activeWindowId.
894
+ if (this._initialized && this.activeWindowId === null) {
895
+ this.logger.info(`Switching to first window @${event.windowId}`);
896
+ await this.switchToWindow(event.windowId);
897
+ }
898
+ else if (this._initialized && isNewWindow && this.activeWindowId !== null) {
899
+ // Runtime window creation (after initial attach)
900
+ this.logger.info(`Switching to new runtime window @${event.windowId}`);
901
+ await this.switchToWindow(event.windowId);
902
+ }
903
+ }
904
+ break;
905
+ case 'window-close':
906
+ if (event.windowId !== undefined) {
907
+ await this.handleWindowClose(event.windowId);
908
+ }
909
+ break;
910
+ case 'pane-add':
911
+ if (event.paneId !== undefined && event.windowId !== undefined) {
912
+ this.logger.info(`Handling pane-add event: pane=${event.paneId}, window=${event.windowId}`);
913
+ await this.handlePaneAdd(event.paneId, event.windowId);
914
+ }
915
+ break;
916
+ case 'pane-update':
917
+ if (event.paneId !== undefined && event.windowId !== undefined) {
918
+ // Pane might have moved to a different window
919
+ this.logger.info(`Handling pane-update event: pane=${event.paneId}, window=${event.windowId}`);
920
+ await this.handlePaneUpdate(event.paneId, event.windowId);
921
+ }
922
+ break;
923
+ case 'pane-close':
924
+ if (event.paneId !== undefined && event.windowId !== undefined) {
925
+ this.logger.info(`Handling pane-close event: pane=${event.paneId}, window=${event.windowId}`);
926
+ this.handlePaneClose(event.paneId, event.windowId);
927
+ }
928
+ break;
929
+ case 'layout-change':
930
+ // NOTE: We always call syncLayout for the active window.
931
+ // For non-active windows, we save the layout but don't rebuild
932
+ // the tree (it will be rebuilt when the user switches to it).
933
+ if (event.windowId !== undefined && ((_b = event.data) === null || _b === void 0 ? void 0 : _b.layout)) {
934
+ if (event.windowId === this.activeWindowId) {
935
+ this.logger.info(`Syncing layout for active window @${event.windowId}`);
936
+ await this.syncLayout(event.data.layout);
937
+ }
938
+ else {
939
+ this.logger.info(`Layout changed for inactive window @${event.windowId}, saved for next switch`);
940
+ }
941
+ }
942
+ break;
943
+ case 'exit':
944
+ this.connected = false;
945
+ this.cdr.detectChanges();
946
+ break;
947
+ }
948
+ }
949
+ /**
950
+ * Switch to a different tmux window.
951
+ * Hides current window's panes and shows target window's panes.
952
+ */
953
+ async switchToWindow(windowId) {
954
+ var _a, _b;
955
+ if (windowId === this.activeWindowId)
956
+ return;
957
+ this.logger.info(`Switching to window @${windowId}`);
958
+ // Clear dividers while switching windows
959
+ this._tmuxDividers = [];
960
+ // 1. Detach current active window's pane views (don't use removeTab —
961
+ // SplitTabComponent.removeTab destroys the tab when root.children
962
+ // becomes empty). Instead, clear the root directly.
963
+ if (this.activeWindowId !== null) {
964
+ const paneMap = this.windowPaneTabs.get(this.activeWindowId);
965
+ if (paneMap) {
966
+ this.logger.info(`Detaching ${paneMap.size} pane(s) for window @${this.activeWindowId}`);
967
+ for (const paneTab of paneMap.values()) {
968
+ paneTab.emitVisibility(false);
969
+ this.detachPaneView(paneTab);
970
+ }
971
+ }
972
+ }
973
+ // 2. Update active window
974
+ this.activeWindowId = windowId;
975
+ // 3. Ensure pane tabs exist for this window
976
+ if (!this.windowPaneTabs.has(windowId)) {
977
+ this.windowPaneTabs.set(windowId, new Map());
978
+ }
979
+ const paneMap = this.windowPaneTabs.get(windowId);
980
+ if (paneMap.size === 0) {
981
+ // First time visiting this window. Pane tabs will be created by
982
+ // handlePaneAdd (triggered by discoverPanesFromLayout on
983
+ // %layout-change). We don't call addPanesForWindow — panes are
984
+ // discovered asynchronously (iTerm2-style).
985
+ //
986
+ // HOWEVER: if the controller already knows the layout for this
987
+ // window (from batch discovery during attach), we can proactively
988
+ // discover panes from it instead of waiting for a layout-change
989
+ // event that may never come (tmux doesn't re-send layout-change
990
+ // for windows that haven't changed).
991
+ const windowState = (_a = this.controller) === null || _a === void 0 ? void 0 : _a.getWindowState(windowId);
992
+ if (windowState === null || windowState === void 0 ? void 0 : windowState.layout) {
993
+ this.logger.info(`No pane tabs yet for window @${windowId}, but layout is known — discovering panes proactively`);
994
+ // Extract pane IDs from the known layout and create pane tabs
995
+ const { parseTmuxLayout, flattenLayout } = await Promise.resolve().then(() => __importStar(__webpack_require__(/*! ../layoutParser */ "./src/layoutParser.ts")));
996
+ const layoutTree = parseTmuxLayout(windowState.layout);
997
+ if (layoutTree) {
998
+ for (const pane of flattenLayout(layoutTree)) {
999
+ if (!paneMap.has(pane.paneId)) {
1000
+ this.logger.info(`Proactively creating pane tab for %${pane.paneId} in window @${windowId}`);
1001
+ const paneTab = this.createPaneTab(pane.paneId);
1002
+ paneTab.controller = this.controller;
1003
+ paneTab.paneId = pane.paneId;
1004
+ paneMap.set(pane.paneId, paneTab);
1005
+ }
1006
+ }
1007
+ }
1008
+ }
1009
+ else {
1010
+ this.logger.info(`No pane tabs yet for window @${windowId}, waiting for pane-add events`);
1011
+ }
1012
+ }
1013
+ else {
1014
+ this.logger.info(`Mounting existing ${paneMap.size} pane(s) for window @${windowId}`);
1015
+ }
1016
+ // 4. Rebuild SplitContainer tree with this window's panes
1017
+ this.root = new tabby_core_1.SplitContainer();
1018
+ this.root.orientation = 'h';
1019
+ const paneTabs = Array.from(paneMap.values());
1020
+ if (paneTabs.length > 0) {
1021
+ this.logger.info(`Adding ${paneTabs.length} pane tab(s) to SplitTab`);
1022
+ for (let i = 0; i < paneTabs.length; i++) {
1023
+ const paneTab = paneTabs[i];
1024
+ if (i === 0) {
1025
+ await this.addTab(paneTab, null, 'r');
1026
+ }
1027
+ else {
1028
+ await this.addTab(paneTab, paneTabs[i - 1], 'r');
1029
+ }
1030
+ paneTab.emitVisibility(true);
1031
+ }
1032
+ // 5. Sync layout from tmux
1033
+ const windowState = (_b = this.controller) === null || _b === void 0 ? void 0 : _b.getWindowState(windowId);
1034
+ if (windowState === null || windowState === void 0 ? void 0 : windowState.layout) {
1035
+ this.logger.info('Syncing layout after mounting panes');
1036
+ await this.syncLayout(windowState.layout);
1037
+ }
1038
+ }
1039
+ // 6. Detect changes; precise client size refresh happens via the
1040
+ // .pane-area ResizeObserver once xterm renders its cell grid.
1041
+ this.cdr.detectChanges();
1042
+ // 7. Push the correct client size to tmux once panes are mounted.
1043
+ // We use requestAnimationFrame to wait for xterm to render its
1044
+ // character grid (getCellSize needs real cell dimensions), then
1045
+ // force a non-deduplicated refresh-client -C.
1046
+ if (paneTabs.length > 0) {
1047
+ requestAnimationFrame(() => {
1048
+ // Reset dedup so the next call always goes through
1049
+ this._lastSentCols = 0;
1050
+ this._lastSentRows = 0;
1051
+ this.refreshClientSize();
1052
+ });
1053
+ }
1054
+ }
1055
+ /**
1056
+ * Override focus to manage which pane is the active (hotkey-target) pane.
1057
+ *
1058
+ * In tmux integration, all panes are visible simultaneously (split layout),
1059
+ * so we cannot blur other tabs (that would prevent their xterm frontends
1060
+ * from staying initialized). Instead, all pane tabs keep `hasFocus = true`
1061
+ * for frontend initialization, and we use `TmuxPaneTabComponent._tmuxActive`
1062
+ * to control which pane processes hotkeys.
1063
+ */
1064
+ focus(tab) {
1065
+ ;
1066
+ this.focusedTab = tab;
1067
+ tab.emitFocused();
1068
+ // Mark only the focused pane as active for hotkey routing.
1069
+ // Other panes remain visible and initialized but won't process
1070
+ // hotkey-triggered input (Ctrl+C, paste, etc.).
1071
+ for (const t of this.getAllTabs()) {
1072
+ if (t instanceof tmuxPaneTab_component_1.TmuxPaneTabComponent) {
1073
+ t._tmuxActive = (t === tab);
1074
+ }
1075
+ }
1076
+ }
1077
+ /**
1078
+ * Detach a pane tab's view from the ViewContainer without calling
1079
+ * removeTab() which would trigger self-destruction when root empties.
1080
+ */
1081
+ detachPaneView(tab) {
1082
+ var _a;
1083
+ // Remove from root tree structure
1084
+ const parent = this.getParentOf(tab);
1085
+ if (parent) {
1086
+ const index = parent.children.indexOf(tab);
1087
+ if (index !== -1) {
1088
+ parent.children.splice(index, 1);
1089
+ parent.ratios.splice(index, 1);
1090
+ }
1091
+ }
1092
+ // Remove the embedded view reference so layout() won't position it
1093
+ ;
1094
+ (_a = this.viewRefs) === null || _a === void 0 ? void 0 : _a.delete(tab);
1095
+ tab.removeFromContainer();
1096
+ tab.parent = null;
1097
+ }
1098
+ /**
1099
+ * Override removeTab to prevent self-destruction when root.children
1100
+ * becomes empty. In TmuxSessionTab, an empty root is normal during
1101
+ * window switches and should not destroy the session tab.
1102
+ */
1103
+ removeTab(tab) {
1104
+ var _a;
1105
+ const parent = this.getParentOf(tab);
1106
+ if (!parent)
1107
+ return;
1108
+ const index = parent.children.indexOf(tab);
1109
+ parent.ratios.splice(index, 1);
1110
+ parent.children.splice(index, 1);
1111
+ tab.removeFromContainer();
1112
+ tab.parent = null;
1113
+ (_a = this.viewRefs) === null || _a === void 0 ? void 0 : _a.delete(tab);
1114
+ this.layout();
1115
+ // Do NOT destroy self when root is empty — this is normal during
1116
+ // tmux window switches.
1117
+ }
1118
+ /**
1119
+ }
1120
+
1121
+ /**
1122
+ * Create a TmuxPaneTabComponent using TabsService (proper Angular DI).
1123
+ * This ensures the component has a hostView and ViewContainerRef.
1124
+ */
1125
+ createPaneTab(paneId) {
1126
+ this.logger.info(`Creating TmuxPaneTabComponent for pane %${paneId}`);
1127
+ const tab = this._tabsService.create({
1128
+ type: tmuxPaneTab_component_1.TmuxPaneTabComponent,
1129
+ inputs: {
1130
+ controller: this.controller,
1131
+ paneId,
1132
+ },
1133
+ });
1134
+ this.logger.info(`TmuxPaneTabComponent created for pane %${paneId}`);
1135
+ return tab;
1136
+ }
1137
+ /**
1138
+ * Handle a new pane being added to a window (real-time from tmux).
1139
+ *
1140
+ * NOTE: We do NOT call addTab here. Instead, we just register the pane in
1141
+ * the map and let syncLayout (called from the %layout-change event that
1142
+ * tmux sends alongside new-pane creation) build the correct tree.
1143
+ * Calling addTab with a fixed direction ('r') would create a wrong tree
1144
+ * structure that syncLayout then has to undo — and the async view
1145
+ * attachment inside addTab races with syncLayout, leaving panes invisible.
1146
+ */
1147
+ async handlePaneAdd(paneId, windowId) {
1148
+ if (!this.controller)
1149
+ return;
1150
+ let paneMap = this.windowPaneTabs.get(windowId);
1151
+ if (!paneMap) {
1152
+ paneMap = new Map();
1153
+ this.windowPaneTabs.set(windowId, paneMap);
1154
+ }
1155
+ if (paneMap.has(paneId)) {
1156
+ this.logger.debug(`Pane %${paneId} already tracked for window @${windowId}`);
1157
+ return;
1158
+ }
1159
+ // Create the pane tab and register it — the actual tree mounting
1160
+ // happens when syncLayout runs from the %layout-change event.
1161
+ const paneTab = this.createPaneTab(paneId);
1162
+ paneTab.controller = this.controller;
1163
+ paneTab.paneId = paneId;
1164
+ paneMap.set(paneId, paneTab);
1165
+ this.logger.info(`Registered new pane %${paneId} for window @${windowId}, awaiting layout sync`);
1166
+ }
1167
+ /**
1168
+ * Handle pane-update event (pane might have moved between windows).
1169
+ *
1170
+ * IMPORTANT: This method MUST NOT trigger switchToWindow or handlePaneAdd.
1171
+ * Doing so creates an infinite loop: pane-update → switchToWindow →
1172
+ * refreshPanes → pane-update → switchToWindow → ...
1173
+ *
1174
+ * Only handle panes already tracked in windowPaneTabs. Untracked panes
1175
+ * will be picked up by handlePaneAdd (from pane-add events triggered
1176
+ * by discoverPanesFromLayout on %layout-change).
1177
+ */
1178
+ handlePaneUpdate(paneId, windowId) {
1179
+ // Find which window currently owns this pane in our map
1180
+ let currentWindowId = null;
1181
+ for (const [wid, paneMap] of this.windowPaneTabs) {
1182
+ if (paneMap.has(paneId)) {
1183
+ currentWindowId = wid;
1184
+ break;
1185
+ }
1186
+ }
1187
+ if (currentWindowId === null) {
1188
+ // Pane not yet tracked — will be added via pane-add or switchToWindow
1189
+ return;
1190
+ }
1191
+ if (currentWindowId === windowId) {
1192
+ // Same window — no action needed
1193
+ return;
1194
+ }
1195
+ // Pane moved between windows — move the tab object
1196
+ this.logger.info(`Moving pane %${paneId} from window @${currentWindowId} to @${windowId}`);
1197
+ const oldPaneMap = this.windowPaneTabs.get(currentWindowId);
1198
+ const paneTab = oldPaneMap.get(paneId);
1199
+ if (paneTab) {
1200
+ oldPaneMap.delete(paneId);
1201
+ let newPaneMap = this.windowPaneTabs.get(windowId);
1202
+ if (!newPaneMap) {
1203
+ newPaneMap = new Map();
1204
+ this.windowPaneTabs.set(windowId, newPaneMap);
1205
+ }
1206
+ newPaneMap.set(paneId, paneTab);
1207
+ // If it was in the active window, remove from SplitTab
1208
+ if (currentWindowId === this.activeWindowId) {
1209
+ paneTab.emitVisibility(false);
1210
+ this.detachPaneView(paneTab);
1211
+ }
1212
+ }
1213
+ }
1214
+ /**
1215
+ * Handle a tmux window being closed.
1216
+ */
1217
+ async handleWindowClose(windowId) {
1218
+ const paneMap = this.windowPaneTabs.get(windowId);
1219
+ if (paneMap) {
1220
+ // Destroy all pane tabs for this window
1221
+ for (const paneTab of paneMap.values()) {
1222
+ if (windowId === this.activeWindowId) {
1223
+ paneTab.emitVisibility(false);
1224
+ this.detachPaneView(paneTab);
1225
+ }
1226
+ paneTab.destroy();
1227
+ }
1228
+ this.windowPaneTabs.delete(windowId);
1229
+ }
1230
+ // If we just closed the active window, switch to another one
1231
+ if (windowId === this.activeWindowId) {
1232
+ this.activeWindowId = null;
1233
+ const remainingWindows = Array.from(this.windowPaneTabs.keys());
1234
+ if (remainingWindows.length > 0) {
1235
+ await this.switchToWindow(remainingWindows[0]);
1236
+ }
1237
+ else {
1238
+ // No windows left — reset
1239
+ this.root = new tabby_core_1.SplitContainer();
1240
+ this.root.orientation = 'h';
1241
+ this.layout();
1242
+ this.cdr.detectChanges();
1243
+ }
1244
+ }
1245
+ }
1246
+ /**
1247
+ * Synchronize SplitTab layout with tmux's layout string.
1248
+ *
1249
+ * This is the SINGLE place where the SplitTab tree is built.
1250
+ * It creates missing pane tabs, attaches their views, cleans up stale
1251
+ * panes, and rebuilds the entire tree from the tmux layout string.
1252
+ */
1253
+ async syncLayout(layoutStr) {
1254
+ var _a;
1255
+ const layoutTree = (0, layoutParser_1.parseTmuxLayout)(layoutStr);
1256
+ if (!layoutTree) {
1257
+ this.logger.warn('Failed to parse layout:', layoutStr);
1258
+ return;
1259
+ }
1260
+ const panes = (0, layoutParser_1.flattenLayout)(layoutTree);
1261
+ this.logger.info(`Syncing layout for window @${this.activeWindowId}: ${panes.length} panes`);
1262
+ // Ensure pane tabs exist and have attached views for every pane in
1263
+ // the layout. New panes (from split-window) are registered by
1264
+ // handlePaneAdd but have no view yet — we call addTab to create one.
1265
+ if (this.activeWindowId !== null) {
1266
+ let paneMap = this.windowPaneTabs.get(this.activeWindowId);
1267
+ if (!paneMap) {
1268
+ paneMap = new Map();
1269
+ this.windowPaneTabs.set(this.activeWindowId, paneMap);
1270
+ }
1271
+ for (const pane of panes) {
1272
+ if (!paneMap.has(pane.paneId)) {
1273
+ this.logger.info(`Creating pane tab for %${pane.paneId} during layout sync`);
1274
+ const paneTab = this.createPaneTab(pane.paneId);
1275
+ paneTab.controller = this.controller;
1276
+ paneTab.paneId = pane.paneId;
1277
+ paneMap.set(pane.paneId, paneTab);
1278
+ }
1279
+ }
1280
+ // Attach views for panes that don't have one yet.
1281
+ // The tree structure will be rebuilt below, so the addTab direction
1282
+ // doesn't matter — we just need the view to exist.
1283
+ for (const pane of panes) {
1284
+ const paneTab = paneMap.get(pane.paneId);
1285
+ if (!((_a = this.viewRefs) === null || _a === void 0 ? void 0 : _a.has(paneTab))) {
1286
+ this.logger.info(`Attaching view for pane %${pane.paneId}`);
1287
+ const existingTabs = this.getAllTabs();
1288
+ if (existingTabs.length === 0) {
1289
+ await this.addTab(paneTab, null, 'r');
1290
+ }
1291
+ else {
1292
+ await this.addTab(paneTab, existingTabs[existingTabs.length - 1], 'r');
1293
+ }
1294
+ ;
1295
+ paneTab.emitVisibility(true);
1296
+ paneTab.emitFocused();
1297
+ }
1298
+ }
1299
+ }
1300
+ // Detect and clean up stale pane tabs that are no longer in the layout.
1301
+ // When tmux closes a pane, the %layout-change notification omits it.
1302
+ // We remove the corresponding tab so the UI stays consistent.
1303
+ if (this.activeWindowId !== null) {
1304
+ const paneMap = this.windowPaneTabs.get(this.activeWindowId);
1305
+ if (paneMap) {
1306
+ const layoutPaneIds = new Set(panes.map(p => p.paneId));
1307
+ for (const [paneId, paneTab] of paneMap) {
1308
+ if (!layoutPaneIds.has(paneId)) {
1309
+ this.logger.info(`Pane %${paneId} no longer in layout, cleaning up`);
1310
+ paneMap.delete(paneId);
1311
+ paneTab.emitVisibility(false);
1312
+ this.detachPaneView(paneTab);
1313
+ paneTab.destroy();
1314
+ }
1315
+ }
1316
+ }
1317
+ }
1318
+ // Build the SplitContainer tree from the parsed layout
1319
+ const newRoot = this.buildSplitContainerFromLayout(layoutTree);
1320
+ if (newRoot instanceof tabby_core_1.SplitContainer) {
1321
+ this.root = newRoot;
1322
+ this.layout();
1323
+ }
1324
+ else if (newRoot) {
1325
+ // Single pane — wrap in a container
1326
+ this.root = new tabby_core_1.SplitContainer();
1327
+ this.root.orientation = 'h';
1328
+ this.root.children.push(newRoot);
1329
+ this.root.ratios.push(1);
1330
+ this.layout();
1331
+ }
1332
+ this.cdr.detectChanges();
1333
+ // tmux is authoritative over each pane's character grid: push the exact
1334
+ // cell dimensions from the layout string into each xterm. This keeps
1335
+ // wrapping aligned with tmux and avoids any pixel-derived resize.
1336
+ this.applyLayoutGrids(layoutTree);
1337
+ }
1338
+ /**
1339
+ * Handle a pane being closed (from %pane-close event or manual cleanup).
1340
+ */
1341
+ handlePaneClose(paneId, windowId) {
1342
+ const paneMap = this.windowPaneTabs.get(windowId);
1343
+ if (!paneMap)
1344
+ return;
1345
+ const paneTab = paneMap.get(paneId);
1346
+ if (!paneTab)
1347
+ return;
1348
+ this.logger.info(`Cleaning up closed pane %${paneId} in window @${windowId}`);
1349
+ paneMap.delete(paneId);
1350
+ if (windowId === this.activeWindowId) {
1351
+ ;
1352
+ paneTab.emitVisibility(false);
1353
+ this.detachPaneView(paneTab);
1354
+ this.layout();
1355
+ this.cdr.detectChanges();
1356
+ }
1357
+ ;
1358
+ paneTab.destroy();
1359
+ }
1360
+ /**
1361
+ * Build a SplitContainer tree from a tmux layout node.
1362
+ */
1363
+ buildSplitContainerFromLayout(node) {
1364
+ if (node.type === 'pane' && node.paneId !== undefined && this.activeWindowId !== null) {
1365
+ const paneMap = this.windowPaneTabs.get(this.activeWindowId);
1366
+ return (paneMap === null || paneMap === void 0 ? void 0 : paneMap.get(node.paneId)) || null;
1367
+ }
1368
+ if (!node.children || node.children.length === 0) {
1369
+ return null;
1370
+ }
1371
+ const container = new tabby_core_1.SplitContainer();
1372
+ container.orientation = node.type === 'horizontal' ? 'h' : 'v';
1373
+ const totalSize = node.type === 'horizontal'
1374
+ ? node.children.reduce((sum, c) => sum + c.width, 0)
1375
+ : node.children.reduce((sum, c) => sum + c.height, 0);
1376
+ for (const child of node.children) {
1377
+ const childComponent = this.buildSplitContainerFromLayout(child);
1378
+ if (childComponent) {
1379
+ container.children.push(childComponent);
1380
+ const ratio = totalSize > 0
1381
+ ? (node.type === 'horizontal' ? child.width / totalSize : child.height / totalSize)
1382
+ : 1 / node.children.length;
1383
+ container.ratios.push(ratio);
1384
+ }
1385
+ }
1386
+ return container.children.length > 0 ? container : null;
1387
+ }
1388
+ /**
1389
+ * Refresh tmux client size based purely on the container (.pane-area) size.
1390
+ *
1391
+ * This is the SINGLE source of truth for the overall client size and is the
1392
+ * key to avoiding the resize feedback loop:
1393
+ *
1394
+ * - We compute the whole-window character grid from the .pane-area pixel
1395
+ * size divided by the xterm cell size. This value depends only on the
1396
+ * container, NOT on tmux's per-pane layout.
1397
+ * - tmux receives this via `refresh-client -C` and decides how to split
1398
+ * the grid among panes (sending %layout-change).
1399
+ * - On %layout-change we set each pane's xterm grid explicitly
1400
+ * (TmuxPaneTabComponent.setTmuxGrid) — panes never fit-to-pixels and
1401
+ * never report a size back up.
1402
+ *
1403
+ * Because the result is derived from the (stable) container size, a tmux
1404
+ * relayout does not change it, so `_lastSentCols/Rows` dedup terminates the
1405
+ * loop after a single iteration.
1406
+ */
1407
+ refreshClientSize() {
1408
+ if (!this.controller || !this._initialized)
1409
+ return;
1410
+ if (this.activeWindowId === null)
1411
+ return;
1412
+ const measured = this.measureClientSize();
1413
+ if (!measured) {
1414
+ // Cell size not available yet (no pane frontend mounted/rendered).
1415
+ // Retry shortly so the first real size still gets sent once a pane
1416
+ // has rendered its character grid — but only if panes are expected.
1417
+ const paneMap = this.windowPaneTabs.get(this.activeWindowId);
1418
+ if (paneMap && paneMap.size > 0) {
1419
+ this.scheduleRefreshClientSize();
1420
+ }
1421
+ return;
1422
+ }
1423
+ const { cols, rows } = measured;
1424
+ if (cols > 0 && rows > 0 &&
1425
+ (cols !== this._lastSentCols || rows !== this._lastSentRows)) {
1426
+ this._lastSentCols = cols;
1427
+ this._lastSentRows = rows;
1428
+ this.logger.info(`Setting tmux client size: ${cols}x${rows}`);
1429
+ this.controller.resizePane(0, cols, rows);
1430
+ }
1431
+ }
1432
+ /**
1433
+ * Apply the tmux-authoritative character grid to each mounted pane.
1434
+ *
1435
+ * tmux's layout string gives the exact width/height (in cells) of every
1436
+ * pane. We push those directly into the corresponding xterm so display
1437
+ * wrapping matches tmux exactly. xterm auto-fit is disabled for tmux panes,
1438
+ * so this is the only thing that sizes them.
1439
+ */
1440
+ applyLayoutGrids(layoutTree) {
1441
+ if (this.activeWindowId === null)
1442
+ return;
1443
+ const paneMap = this.windowPaneTabs.get(this.activeWindowId);
1444
+ if (!paneMap)
1445
+ return;
1446
+ for (const pane of (0, layoutParser_1.flattenLayout)(layoutTree)) {
1447
+ const paneTab = paneMap.get(pane.paneId);
1448
+ if (paneTab === null || paneTab === void 0 ? void 0 : paneTab.setTmuxGrid) {
1449
+ paneTab.setTmuxGrid(pane.width, pane.height);
1450
+ }
1451
+ }
1452
+ }
1453
+ /**
1454
+ * Measure the whole-window character grid from the .pane-area container.
1455
+ *
1456
+ * The container width includes per-pane decorations (xterm scrollbar +
1457
+ * padding) and UI spanner dividers, none of which belong to the tmux
1458
+ * character grid. We subtract them, divide by the real xterm cell size,
1459
+ * then add tmux's 1-char dividers between panes so tmux's own grid lines up.
1460
+ */
1461
+ measureClientSize() {
1462
+ var _a, _b;
1463
+ const host = this.hostElement.nativeElement;
1464
+ const paneArea = (_a = host.querySelector('.pane-area')) !== null && _a !== void 0 ? _a : host;
1465
+ const rect = paneArea.getBoundingClientRect();
1466
+ if (rect.width < 10 || rect.height < 10)
1467
+ return null;
1468
+ const cell = this.getCellSize();
1469
+ if (!cell)
1470
+ return null;
1471
+ // Determine pane/split counts for the active window.
1472
+ const paneMap = this.activeWindowId !== null
1473
+ ? this.windowPaneTabs.get(this.activeWindowId)
1474
+ : null;
1475
+ const paneCount = (_b = paneMap === null || paneMap === void 0 ? void 0 : paneMap.size) !== null && _b !== void 0 ? _b : 1;
1476
+ // UI spanner pixel widths: tabby's split-tab-spanner is 10px each.
1477
+ // Our custom tmux dividers are purely visual overlays with no pixel cost.
1478
+ // _spanners is still populated by layoutInternal() for tabby's internal
1479
+ // bookkeeping, but we no longer render split-tab-spanner elements, so
1480
+ // their pixel width should not be subtracted here.
1481
+ const spannerPx = 0;
1482
+ // Per-pane visual padding defined in tmuxPaneTab.component.scss.
1483
+ // Each pane has 4px on all sides (8px total per axis).
1484
+ // For N panes in a row: total horizontal padding = N * 8px.
1485
+ // For N panes in a column: total vertical padding = N * 8px.
1486
+ // Approximate as all panes contributing to both axes (safe overestimate).
1487
+ const panePadPerAxis = 8;
1488
+ const totalPadPx = paneCount * panePadPerAxis;
1489
+ const availableWidth = rect.width - spannerPx - totalPadPx;
1490
+ const availableHeight = rect.height - totalPadPx;
1491
+ const contentCols = Math.floor(availableWidth / cell.width);
1492
+ const contentRows = Math.floor(availableHeight / cell.height);
1493
+ // tmux inserts a 1-char divider between adjacent panes; add them back so
1494
+ // the size we report covers content + dividers (tmux subtracts them again
1495
+ // when splitting). Approximate as a horizontal split (most common case).
1496
+ const numDividers = Math.max(0, paneCount - 1);
1497
+ const cols = contentCols + numDividers;
1498
+ return {
1499
+ cols: Math.max(2, cols),
1500
+ rows: Math.max(1, contentRows),
1501
+ };
1502
+ }
1503
+ /**
1504
+ * Read the xterm character cell size (in CSS pixels) from any mounted pane.
1505
+ */
1506
+ getCellSize() {
1507
+ var _a, _b, _c, _d, _e, _f;
1508
+ for (const paneMap of this.windowPaneTabs.values()) {
1509
+ for (const paneTab of paneMap.values()) {
1510
+ const frontend = paneTab.frontend;
1511
+ const dims = (_b = (_a = frontend === null || frontend === void 0 ? void 0 : frontend.xtermCore) === null || _a === void 0 ? void 0 : _a._renderService) === null || _b === void 0 ? void 0 : _b.dimensions;
1512
+ if (((_d = (_c = dims === null || dims === void 0 ? void 0 : dims.css) === null || _c === void 0 ? void 0 : _c.cell) === null || _d === void 0 ? void 0 : _d.width) > 0 && ((_f = (_e = dims === null || dims === void 0 ? void 0 : dims.css) === null || _e === void 0 ? void 0 : _e.cell) === null || _f === void 0 ? void 0 : _f.height) > 0) {
1513
+ return { width: dims.css.cell.width, height: dims.css.cell.height };
1514
+ }
1515
+ }
1516
+ }
1517
+ return null;
1518
+ }
1519
+ /**
1520
+ * Debounced version of refreshClientSize.
1521
+ * Multiple sources (window resize, switchToWindow, layout-change) may
1522
+ * fire close together — debounce into one refresh-client -C call.
1523
+ */
1524
+ scheduleRefreshClientSize() {
1525
+ var _a, _b;
1526
+ if (this._resizeTimer)
1527
+ clearTimeout(this._resizeTimer);
1528
+ const debounceMs = (_b = (_a = this.configService.store.tmuxPlugin) === null || _a === void 0 ? void 0 : _a.resizeDebounceMs) !== null && _b !== void 0 ? _b : 150;
1529
+ this._resizeTimer = setTimeout(() => {
1530
+ this._resizeTimer = null;
1531
+ this.refreshClientSize();
1532
+ }, debounceMs);
1533
+ }
1534
+ /**
1535
+ * Attach mousemove + mousedown handlers to .pane-area so that hovering
1536
+ * near a .child's right/bottom border highlights it, and dragging starts
1537
+ * a tmux resize-pane operation.
1538
+ */
1539
+ attachPaneAreaBorderHandlers() {
1540
+ const host = this.hostElement.nativeElement;
1541
+ const paneArea = host.querySelector('.pane-area');
1542
+ if (!paneArea)
1543
+ return;
1544
+ const HIT = TmuxSessionTabComponent_1.BORDER_HIT;
1545
+ // Track which child + edge is currently hovered
1546
+ let hoveredChild = null;
1547
+ let hoveredEdge = null;
1548
+ const clearHover = () => {
1549
+ if (hoveredChild) {
1550
+ hoveredChild.classList.remove('border-hover-right', 'border-hover-bottom');
1551
+ }
1552
+ hoveredChild = null;
1553
+ hoveredEdge = null;
1554
+ paneArea.style.cursor = '';
1555
+ };
1556
+ const onMove = (e) => {
1557
+ const areaRect = paneArea.getBoundingClientRect();
1558
+ const mx = e.clientX - areaRect.left;
1559
+ const my = e.clientY - areaRect.top;
1560
+ // Find a .child whose right or bottom border is within HIT pixels
1561
+ clearHover();
1562
+ const children = paneArea.querySelectorAll('.child');
1563
+ for (const child of Array.from(children)) {
1564
+ const el = child;
1565
+ const r = el.getBoundingClientRect();
1566
+ const right = r.right - areaRect.left;
1567
+ const bottom = r.bottom - areaRect.top;
1568
+ const left = r.left - areaRect.left;
1569
+ const top = r.top - areaRect.top;
1570
+ // Right border hit: within HIT of right edge, vertically inside
1571
+ if (Math.abs(mx - right) <= HIT && my >= top && my <= bottom) {
1572
+ el.classList.add('border-hover-right');
1573
+ paneArea.style.cursor = 'col-resize';
1574
+ hoveredChild = el;
1575
+ hoveredEdge = 'right';
1576
+ break;
1577
+ }
1578
+ // Bottom border hit: within HIT of bottom edge, horizontally inside
1579
+ if (Math.abs(my - bottom) <= HIT && mx >= left && mx <= right) {
1580
+ el.classList.add('border-hover-bottom');
1581
+ paneArea.style.cursor = 'row-resize';
1582
+ hoveredChild = el;
1583
+ hoveredEdge = 'bottom';
1584
+ break;
1585
+ }
1586
+ }
1587
+ };
1588
+ const onDown = (e) => {
1589
+ if (!hoveredChild || !hoveredEdge || !this.controller)
1590
+ return;
1591
+ e.preventDefault();
1592
+ e.stopPropagation();
1593
+ const edge = hoveredEdge;
1594
+ const cell = this.getCellSize();
1595
+ if (!cell)
1596
+ return;
1597
+ const startX = e.clientX;
1598
+ const startY = e.clientY;
1599
+ // Find the pane ID for this .child element
1600
+ const resizeTarget = hoveredChild;
1601
+ const paneId = this.findPaneIdForElement(resizeTarget);
1602
+ if (paneId === null)
1603
+ return;
1604
+ // Track last sent delta to send incremental resize commands
1605
+ let lastSentCols = 0;
1606
+ let lastSentRows = 0;
1607
+ const onDragMove = (de) => {
1608
+ document.body.style.cursor = edge === 'right' ? 'col-resize' : 'row-resize';
1609
+ if (edge === 'right') {
1610
+ const deltaCols = Math.round((de.clientX - startX) / cell.width);
1611
+ if (deltaCols !== lastSentCols) {
1612
+ const diff = deltaCols - lastSentCols;
1613
+ const flag = diff > 0 ? '-R' : '-L';
1614
+ this.controller.gateway.sendCommand(`resize-pane ${flag} -t %${paneId} ${Math.abs(diff)}`);
1615
+ lastSentCols = deltaCols;
1616
+ }
1617
+ }
1618
+ else {
1619
+ const deltaRows = Math.round((de.clientY - startY) / cell.height);
1620
+ if (deltaRows !== lastSentRows) {
1621
+ const diff = deltaRows - lastSentRows;
1622
+ const flag = diff > 0 ? '-D' : '-U';
1623
+ this.controller.gateway.sendCommand(`resize-pane ${flag} -t %${paneId} ${Math.abs(diff)}`);
1624
+ lastSentRows = deltaRows;
1625
+ }
1626
+ }
1627
+ };
1628
+ const onDragUp = () => {
1629
+ document.removeEventListener('mousemove', onDragMove);
1630
+ document.removeEventListener('mouseup', onDragUp);
1631
+ document.body.style.cursor = '';
1632
+ clearHover();
1633
+ };
1634
+ document.addEventListener('mousemove', onDragMove);
1635
+ document.addEventListener('mouseup', onDragUp);
1636
+ };
1637
+ const onLeave = () => clearHover();
1638
+ this._paneAreaMouseMoveHandler = onMove;
1639
+ this._paneAreaMouseDownHandler = onDown;
1640
+ paneArea.addEventListener('mousemove', onMove);
1641
+ paneArea.addEventListener('mousedown', onDown);
1642
+ paneArea.addEventListener('mouseleave', onLeave);
1643
+ }
1644
+ detachPaneAreaBorderHandlers() {
1645
+ const host = this.hostElement.nativeElement;
1646
+ const paneArea = host.querySelector('.pane-area');
1647
+ if (!paneArea)
1648
+ return;
1649
+ if (this._paneAreaMouseMoveHandler) {
1650
+ paneArea.removeEventListener('mousemove', this._paneAreaMouseMoveHandler);
1651
+ this._paneAreaMouseMoveHandler = null;
1652
+ }
1653
+ if (this._paneAreaMouseDownHandler) {
1654
+ paneArea.removeEventListener('mousedown', this._paneAreaMouseDownHandler);
1655
+ this._paneAreaMouseDownHandler = null;
1656
+ }
1657
+ }
1658
+ /**
1659
+ * Map a .child DOM element back to the tmux pane ID it represents.
1660
+ */
1661
+ findPaneIdForElement(el) {
1662
+ var _a, _b, _c;
1663
+ if (this.activeWindowId === null)
1664
+ return null;
1665
+ const paneMap = this.windowPaneTabs.get(this.activeWindowId);
1666
+ if (!paneMap)
1667
+ return null;
1668
+ for (const [paneId, paneTab] of paneMap) {
1669
+ const tabEl = (_b = (_a = paneTab.hostElement) === null || _a === void 0 ? void 0 : _a.nativeElement) !== null && _b !== void 0 ? _b : (_c = paneTab.element) === null || _c === void 0 ? void 0 : _c.nativeElement;
1670
+ if (tabEl && (tabEl === el || tabEl.contains(el) || el.contains(tabEl))) {
1671
+ return paneId;
1672
+ }
1673
+ }
1674
+ return null;
1675
+ }
1676
+ // ─── (legacy divider stubs removed) ─────────────────────────────────────
1677
+ /**
1678
+ * Override onSpannerAdjusted to notify tmux of layout change.
1679
+ * When the user drags a spanner (split divider), the pane containers
1680
+ * resize and xterm.js auto-fits. We need to tell tmux the new client size
1681
+ * so it can recalculate its layout accordingly.
1682
+ */
1683
+ onSpannerAdjusted(spanner) {
1684
+ super.onSpannerAdjusted(spanner);
1685
+ this.scheduleRefreshClientSize();
1686
+ }
1687
+ // --- UI Event Handlers ---
1688
+ onDisconnect() {
1689
+ const ctx = this.tmuxService.findContextForTab(this);
1690
+ if (ctx) {
1691
+ this.tmuxService.disconnectContext(ctx);
1692
+ }
1693
+ }
1694
+ async onWindowClose(windowId) {
1695
+ if (this.controller) {
1696
+ await this.controller.killWindow(windowId);
1697
+ }
1698
+ }
1699
+ async onCreateWindow() {
1700
+ if (this.controller) {
1701
+ const newWindowId = await this.controller.createWindow();
1702
+ if (newWindowId !== null) {
1703
+ await this.switchToWindow(newWindowId);
1704
+ }
1705
+ }
1706
+ }
1707
+ ngOnDestroy() {
1708
+ if (this.eventSubscription) {
1709
+ this.eventSubscription.unsubscribe();
1710
+ }
1711
+ if (this._resizeHandler) {
1712
+ window.removeEventListener('resize', this._resizeHandler);
1713
+ this._resizeHandler = null;
1714
+ }
1715
+ if (this._paneAreaObserver) {
1716
+ this._paneAreaObserver.disconnect();
1717
+ this._paneAreaObserver = null;
1718
+ }
1719
+ if (this._resizeTimer) {
1720
+ clearTimeout(this._resizeTimer);
1721
+ this._resizeTimer = null;
1722
+ }
1723
+ this.detachPaneAreaBorderHandlers();
1724
+ super.ngOnDestroy();
1725
+ }
1726
+ async canClose() {
1727
+ return true;
1728
+ }
1729
+ /**
1730
+ * Override recovery to delegate to the hidden host tab.
1731
+ *
1732
+ * When Tabby saves tabs on exit, the original terminal tab (topmostTab)
1733
+ * is hidden from app.tabs and would be lost. Instead of persisting the
1734
+ * tmux session tab (which cannot be meaningfully restored), we return
1735
+ * the host tab's recovery token so Tabby restores the pre-tmux terminal.
1736
+ * The tmux session remains alive in the background and can be re-attached.
1737
+ */
1738
+ async getRecoveryToken(options) {
1739
+ const ctx = this.tmuxService.findContextForTab(this);
1740
+ if (ctx === null || ctx === void 0 ? void 0 : ctx.topmostTab) {
1741
+ return ctx.topmostTab.getRecoveryToken(options);
1742
+ }
1743
+ return null;
1744
+ }
1745
+ };
1746
+ // ─── Border-based pane separator ────────────────────────────────────────
1747
+ /** Pixels from the right/bottom edge of a .child that counts as "on border" */
1748
+ TmuxSessionTabComponent.BORDER_HIT = 10;
1749
+ TmuxSessionTabComponent.ctorParameters = () => [
1750
+ { type: core_1.Injector },
1751
+ { type: tmux_service_1.TmuxService },
1752
+ { type: tabby_core_1.ConfigService },
1753
+ { type: tabby_core_1.TabsService },
1754
+ { type: core_1.ChangeDetectorRef },
1755
+ { type: core_1.ElementRef },
1756
+ { type: tabby_core_1.LogService }
1757
+ ];
1758
+ TmuxSessionTabComponent.propDecorators = {
1759
+ profile: [{ type: core_1.Input }],
1760
+ existingController: [{ type: core_1.Input }]
1761
+ };
1762
+ TmuxSessionTabComponent = TmuxSessionTabComponent_1 = __decorate([
1763
+ (0, core_1.Component)({
1764
+ selector: 'tmux-session-tab',
1765
+ host: {
1766
+ '[class.tmux-session-host]': 'true'
1767
+ },
1768
+ template: `
1769
+ <div class="pane-area" #paneAreaEl>
1770
+ <ng-container #vc></ng-container>
1771
+ </div>
1772
+ <tmux-window-bar
1773
+ [controller]="controller"
1774
+ [activeWindowId]="activeWindowId"
1775
+ (windowSwitch)="switchToWindow($event)"
1776
+ (windowClose)="onWindowClose($event)"
1777
+ (disconnect)="onDisconnect()"
1778
+ (createWindow)="onCreateWindow()"
1779
+ ></tmux-window-bar>
1780
+ `,
1781
+ styles: ["\n :host {\n position: relative;\n display: flex;\n flex-direction: column;\n width: 100%;\n height: 100%;\n }\n .pane-area {\n flex: 1 1 0;\n position: relative;\n min-height: 0;\n }\n /* SplitTab.layoutInternal() positions .child with inline left/top/width/height %.\n border-right + border-bottom render the tmux pane separator line.\n box-sizing: border-box keeps the border inside the layout box so\n xterm content is not displaced.\n The border area also serves as the resize drag handle (see onPaneAreaMouseDown). */\n ::ng-deep .pane-area > .child {\n position: absolute;\n transition: 0.125s all;\n opacity: .75;\n box-sizing: border-box;\n border-right: 1px solid rgba(128,128,128,0.3);\n border-bottom: 1px solid rgba(128,128,128,0.3);\n }\n ::ng-deep .pane-area > .child.focused {\n opacity: 1;\n }\n /*\n * Transparent hit-target overlays at the right/bottom edges.\n * xterm renders a scrollbar (~14px wide) that intercepts mouse events,\n * preventing the pane-area mousemove handler from detecting border\n * proximity. These ::after pseudo-elements sit above the scrollbar\n * (z-index: 10) and capture events for resize dragging.\n */\n ::ng-deep .pane-area > .child::after {\n content: '';\n position: absolute;\n top: 0;\n right: 0;\n width: 10px;\n height: 100%;\n z-index: 10;\n pointer-events: auto;\n cursor: col-resize;\n }\n ::ng-deep .pane-area > .child::before {\n content: '';\n position: absolute;\n bottom: 0;\n left: 0;\n width: 100%;\n height: 10px;\n z-index: 10;\n pointer-events: auto;\n cursor: row-resize;\n }\n /* Highlight the border when hovering near the right/bottom edge */\n ::ng-deep .pane-area > .child.border-hover-right {\n border-right-color: rgba(128,128,128,0.75);\n }\n ::ng-deep .pane-area > .child.border-hover-bottom {\n border-bottom-color: rgba(128,128,128,0.75);\n }\n tmux-window-bar {\n flex: 0 0 auto;\n position: relative;\n z-index: 10;\n }\n "]
1782
+ }),
1783
+ __metadata("design:paramtypes", [core_1.Injector,
1784
+ tmux_service_1.TmuxService,
1785
+ tabby_core_1.ConfigService,
1786
+ tabby_core_1.TabsService,
1787
+ core_1.ChangeDetectorRef,
1788
+ core_1.ElementRef,
1789
+ tabby_core_1.LogService])
1790
+ ], TmuxSessionTabComponent);
1791
+ exports.TmuxSessionTabComponent = TmuxSessionTabComponent;
1792
+
1793
+
1794
+ /***/ },
1795
+
1796
+ /***/ "./src/components/tmuxWindowBar.component.ts"
1797
+ /*!***************************************************!*\
1798
+ !*** ./src/components/tmuxWindowBar.component.ts ***!
1799
+ \***************************************************/
1800
+ (__unused_webpack_module, exports, __webpack_require__) {
1801
+
1802
+ "use strict";
1803
+
1804
+ var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
1805
+ var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
1806
+ if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
1807
+ else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
1808
+ return c > 3 && r && Object.defineProperty(target, key, r), r;
1809
+ };
1810
+ var __metadata = (this && this.__metadata) || function (k, v) {
1811
+ if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
1812
+ };
1813
+ Object.defineProperty(exports, "__esModule", ({ value: true }));
1814
+ exports.TmuxWindowBarComponent = void 0;
1815
+ const core_1 = __webpack_require__(/*! @angular/core */ "@angular/core");
1816
+ const session_1 = __webpack_require__(/*! ../session */ "./src/session.ts");
1817
+ let TmuxWindowBarComponent = class TmuxWindowBarComponent {
1818
+ constructor(cdr) {
1819
+ this.cdr = cdr;
1820
+ this.activeWindowId = null;
1821
+ this.windowSwitch = new core_1.EventEmitter();
1822
+ this.windowClose = new core_1.EventEmitter();
1823
+ this.disconnect = new core_1.EventEmitter();
1824
+ this.createWindow = new core_1.EventEmitter();
1825
+ this.windows = [];
1826
+ }
1827
+ ngOnInit() {
1828
+ this.refreshWindows();
1829
+ if (!this.controller) {
1830
+ return;
1831
+ }
1832
+ this.subscription = this.controller.events.subscribe(event => {
1833
+ switch (event.type) {
1834
+ case 'window-add':
1835
+ case 'window-close':
1836
+ case 'window-renamed':
1837
+ case 'pane-add':
1838
+ case 'pane-close':
1839
+ case 'initialized':
1840
+ this.refreshWindows();
1841
+ break;
1842
+ }
1843
+ });
1844
+ }
1845
+ ngOnDestroy() {
1846
+ var _a;
1847
+ (_a = this.subscription) === null || _a === void 0 ? void 0 : _a.unsubscribe();
1848
+ }
1849
+ refreshWindows() {
1850
+ if (!this.controller) {
1851
+ this.windows = [];
1852
+ this.cdr.detectChanges();
1853
+ return;
1854
+ }
1855
+ const windowStates = this.controller.getAllWindowStates();
1856
+ this.windows = windowStates.map(ws => ({
1857
+ id: ws.id,
1858
+ name: ws.name,
1859
+ paneCount: ws.panes.size,
1860
+ }));
1861
+ this.cdr.detectChanges();
1862
+ }
1863
+ onCloseWindow(event, win) {
1864
+ event.stopPropagation();
1865
+ this.windowClose.emit(win.id);
1866
+ }
1867
+ onContextMenu(event, _win) {
1868
+ // Reserved for future context menu (rename, close window, etc.)
1869
+ event.preventDefault();
1870
+ }
1871
+ };
1872
+ TmuxWindowBarComponent.ctorParameters = () => [
1873
+ { type: core_1.ChangeDetectorRef }
1874
+ ];
1875
+ TmuxWindowBarComponent.propDecorators = {
1876
+ controller: [{ type: core_1.Input }],
1877
+ activeWindowId: [{ type: core_1.Input }],
1878
+ windowSwitch: [{ type: core_1.Output }],
1879
+ windowClose: [{ type: core_1.Output }],
1880
+ disconnect: [{ type: core_1.Output }],
1881
+ createWindow: [{ type: core_1.Output }]
1882
+ };
1883
+ TmuxWindowBarComponent = __decorate([
1884
+ (0, core_1.Component)({
1885
+ selector: 'tmux-window-bar',
1886
+ template: `
1887
+ <div class="window-bar">
1888
+ <div class="window-tabs">
1889
+ <button
1890
+ *ngFor="let win of windows"
1891
+ class="window-tab"
1892
+ [class.active]="win.id === activeWindowId"
1893
+ (click)="windowSwitch.emit(win.id)"
1894
+ (contextmenu)="onContextMenu($event, win)"
1895
+ [title]="win.name"
1896
+ >
1897
+ <span class="window-name">{{ win.name }}</span>
1898
+ <span class="pane-badge" *ngIf="win.paneCount > 1">{{ win.paneCount }}</span>
1899
+ <span class="window-close" title="Close Window" (click)="onCloseWindow($event, win)">
1900
+ <i class="fas fa-times"></i>
1901
+ </span>
1902
+ </button>
1903
+ <button class="window-tab add-btn" title="New Window" (click)="createWindow.emit()">
1904
+ <i class="fas fa-plus"></i>
1905
+ </button>
1906
+ </div>
1907
+ <div class="bar-actions">
1908
+ <button class="bar-btn" title="Disconnect" (click)="disconnect.emit()">
1909
+ <i class="fas fa-eject"></i>
1910
+ </button>
1911
+ </div>
1912
+ </div>
1913
+ `,
1914
+ styles: ["\n :host {\n display: block;\n flex: 0 0 auto;\n }\n .window-bar {\n display: flex;\n align-items: center;\n justify-content: space-between;\n padding: 2px 8px;\n background: rgba(30, 30, 30, 0.95);\n border-top: 1px solid rgba(255, 255, 255, 0.1);\n min-height: 28px;\n overflow-x: auto;\n }\n .window-tabs {\n display: flex;\n align-items: center;\n gap: 2px;\n overflow-x: auto;\n flex: 1;\n min-width: 0;\n }\n .window-tab {\n display: flex;\n align-items: center;\n gap: 4px;\n height: 22px;\n padding: 0 8px;\n border: 1px solid transparent;\n border-radius: 3px;\n background: transparent;\n color: #999;\n font-size: 0.82em;\n cursor: pointer;\n white-space: nowrap;\n transition: background 0.15s, color 0.15s, border-color 0.15s;\n }\n .window-tab:hover {\n background: rgba(255, 255, 255, 0.08);\n color: #ccc;\n }\n .window-tab.active {\n background: rgba(255, 255, 255, 0.12);\n color: #fff;\n border-color: rgba(255, 255, 255, 0.15);\n }\n .pane-badge {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n min-width: 16px;\n height: 16px;\n padding: 0 3px;\n border-radius: 8px;\n background: rgba(255, 255, 255, 0.1);\n font-size: 0.85em;\n color: #aaa;\n }\n .window-close {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n margin-left: auto;\n margin-right: -4px;\n width: 14px;\n height: 14px;\n border-radius: 2px;\n font-size: 0.7em;\n color: transparent;\n cursor: pointer;\n visibility: hidden;\n }\n .window-tab:hover .window-close {\n visibility: visible;\n color: #888;\n }\n .window-close:hover {\n background: rgba(255, 80, 80, 0.3);\n color: #f66;\n }\n .add-btn {\n color: #666;\n padding: 0 6px;\n }\n .add-btn:hover {\n color: #aaa;\n }\n .bar-actions {\n display: flex;\n align-items: center;\n gap: 2px;\n margin-left: 8px;\n }\n .bar-btn {\n display: flex;\n align-items: center;\n justify-content: center;\n width: 24px;\n height: 24px;\n border: none;\n border-radius: 3px;\n background: transparent;\n color: #888;\n font-size: 0.8em;\n cursor: pointer;\n }\n .bar-btn:hover {\n background: rgba(255, 255, 255, 0.1);\n color: #ccc;\n }\n "]
1915
+ }),
1916
+ __metadata("design:paramtypes", [core_1.ChangeDetectorRef])
1917
+ ], TmuxWindowBarComponent);
1918
+ exports.TmuxWindowBarComponent = TmuxWindowBarComponent;
1919
+
1920
+
1921
+ /***/ },
1922
+
1923
+ /***/ "./src/config.ts"
1924
+ /*!***********************!*\
1925
+ !*** ./src/config.ts ***!
1926
+ \***********************/
1927
+ (__unused_webpack_module, exports, __webpack_require__) {
1928
+
1929
+ "use strict";
1930
+
1931
+ Object.defineProperty(exports, "__esModule", ({ value: true }));
1932
+ exports.TmuxConfigProvider = void 0;
1933
+ const tabby_core_1 = __webpack_require__(/*! tabby-core */ "tabby-core");
1934
+ // eslint-disable-next-line new-cap
1935
+ class TmuxConfigProvider extends tabby_core_1.ConfigProvider {
1936
+ constructor() {
1937
+ super(...arguments);
1938
+ this.defaults = {
1939
+ tmuxPlugin: {
1940
+ defaultSessionName: 'default',
1941
+ commandTimeoutMs: 30000,
1942
+ sendKeysChunkSize: 200,
1943
+ resizeDebounceMs: 150,
1944
+ debugLogging: false,
1945
+ },
1946
+ };
1947
+ }
1948
+ }
1949
+ exports.TmuxConfigProvider = TmuxConfigProvider;
1950
+
1951
+
1952
+ /***/ },
1953
+
1954
+ /***/ "./src/gateway.ts"
1955
+ /*!************************!*\
1956
+ !*** ./src/gateway.ts ***!
1957
+ \************************/
1958
+ (__unused_webpack_module, exports, __webpack_require__) {
1959
+
1960
+ "use strict";
1961
+
1962
+ Object.defineProperty(exports, "__esModule", ({ value: true }));
1963
+ exports.TmuxGateway = exports.TMUX_COMMAND_WANTS_DATA = exports.TMUX_COMMAND_TOLERATE_ERRORS = void 0;
1964
+ const rxjs_1 = __webpack_require__(/*! rxjs */ "rxjs");
1965
+ const logHelper_1 = __webpack_require__(/*! ./logHelper */ "./src/logHelper.ts");
1966
+ // Command flags
1967
+ exports.TMUX_COMMAND_TOLERATE_ERRORS = 1 << 0;
1968
+ exports.TMUX_COMMAND_WANTS_DATA = 1 << 1;
1969
+ const DEFAULT_COMMAND_TIMEOUT_MS = 30000;
1970
+ const DEFAULT_SEND_KEYS_CHUNK_SIZE = 200;
1971
+ /**
1972
+ * TmuxGateway - Protocol layer for tmux control mode
1973
+ *
1974
+ * Handles:
1975
+ * - Command queuing with response matching
1976
+ * - Protocol parsing (%begin/%end/%error blocks)
1977
+ * - Notification dispatch
1978
+ * - Key encoding and sending
1979
+ */
1980
+ class TmuxGateway {
1981
+ constructor(logger, writer, configService) {
1982
+ this.logger = logger;
1983
+ this.writer = writer;
1984
+ this.configService = configService;
1985
+ this.commandQueue = [];
1986
+ this.currentCommand = null;
1987
+ this.currentCommandId = '';
1988
+ this.currentResponse = [];
1989
+ this.inResponseBlock = false;
1990
+ this.nextCommandId = 1;
1991
+ this.disconnected = false;
1992
+ this.detachSent = false;
1993
+ this.acceptNotifications = false;
1994
+ this.initialized = false;
1995
+ /** Number of fire-and-forget writes whose %begin/%end responses need consuming */
1996
+ this.directWritesPending = 0;
1997
+ /** Incomplete line buffer for byte-level DCS parsing */
1998
+ this.lineBuffer = '';
1999
+ this.minimumServerVersion = null;
2000
+ this.maximumServerVersion = null;
2001
+ this.pauseModeEnabled = false;
2002
+ // Events for notifications
2003
+ this.output$ = new rxjs_1.Subject();
2004
+ this.layoutChange$ = new rxjs_1.Subject();
2005
+ this.windowAdd$ = new rxjs_1.Subject();
2006
+ this.windowClose$ = new rxjs_1.Subject();
2007
+ this.windowRenamed$ = new rxjs_1.Subject();
2008
+ this.sessionChanged$ = new rxjs_1.Subject();
2009
+ this.sessionsChanged$ = new rxjs_1.Subject();
2010
+ this.paneChanged$ = new rxjs_1.Subject();
2011
+ this.sessionWindowChanged$ = new rxjs_1.Subject();
2012
+ this.paneClose$ = new rxjs_1.Subject();
2013
+ this.exit$ = new rxjs_1.Subject();
2014
+ this.initialized$ = new rxjs_1.Subject();
2015
+ }
2016
+ get log() {
2017
+ return (0, logHelper_1.createConditionalLogger)(this.logger, this.configService);
2018
+ }
2019
+ get commandTimeoutMs() {
2020
+ var _a, _b, _c;
2021
+ return (_c = (_b = (_a = this.configService) === null || _a === void 0 ? void 0 : _a.store.tmuxPlugin) === null || _b === void 0 ? void 0 : _b.commandTimeoutMs) !== null && _c !== void 0 ? _c : DEFAULT_COMMAND_TIMEOUT_MS;
2022
+ }
2023
+ get sendKeysChunkSize() {
2024
+ var _a, _b, _c;
2025
+ return (_c = (_b = (_a = this.configService) === null || _a === void 0 ? void 0 : _a.store.tmuxPlugin) === null || _b === void 0 ? void 0 : _b.sendKeysChunkSize) !== null && _c !== void 0 ? _c : DEFAULT_SEND_KEYS_CHUNK_SIZE;
2026
+ }
2027
+ /**
2028
+ * Send a single command and wait for response
2029
+ */
2030
+ async sendCommand(command, flags = 0) {
2031
+ if (this.detachSent || this.disconnected) {
2032
+ throw new Error('Gateway disconnected');
2033
+ }
2034
+ const original = new Promise((resolve, reject) => {
2035
+ const cmd = {
2036
+ id: this.nextCommandId++,
2037
+ command,
2038
+ resolve,
2039
+ reject,
2040
+ flags,
2041
+ timestamp: Date.now()
2042
+ };
2043
+ this.commandQueue.push(cmd);
2044
+ this.write(command + '\r');
2045
+ this.log.debug(`Sent command: ${command}`);
2046
+ });
2047
+ // Race against timeout — if tmux never responds, reject instead of hanging
2048
+ let timer;
2049
+ const timeout = new Promise((_, reject) => {
2050
+ timer = setTimeout(() => {
2051
+ // Remove the timed-out command from the queue so it won't
2052
+ // consume a later response and cause command-id mismatch.
2053
+ reject(new Error(`Command timed out after ${this.commandTimeoutMs}ms: ${command}`));
2054
+ }, this.commandTimeoutMs);
2055
+ });
2056
+ // Clean up timer when original settles
2057
+ original.then(() => clearTimeout(timer), () => clearTimeout(timer));
2058
+ return Promise.race([original, timeout]);
2059
+ }
2060
+ /**
2061
+ * Send a list of commands atomically (separated by ;)
2062
+ */
2063
+ async sendCommandList(commands) {
2064
+ if (this.detachSent || this.disconnected || commands.length === 0) {
2065
+ return [];
2066
+ }
2067
+ const promises = [];
2068
+ const combined = commands.map((c) => {
2069
+ const promise = new Promise((resolve, reject) => {
2070
+ const cmd = {
2071
+ id: this.nextCommandId++,
2072
+ command: c.command,
2073
+ resolve,
2074
+ reject,
2075
+ flags: c.flags || 0,
2076
+ timestamp: Date.now()
2077
+ };
2078
+ this.commandQueue.push(cmd);
2079
+ });
2080
+ promises.push(promise);
2081
+ return c.command;
2082
+ }).join('; ');
2083
+ this.write(combined + '\r');
2084
+ this.log.debug(`Sent command list: ${combined}`);
2085
+ return Promise.all(promises);
2086
+ }
2087
+ /**
2088
+ * Send keystrokes to a specific pane.
2089
+ *
2090
+ * Writes directly to the PTY — bypasses the command queue for zero-latency
2091
+ * input. tmux will still send %begin/%end for the send-keys command;
2092
+ * parseBegin() tracks these via directWritesPending so they are consumed
2093
+ * without trying to dequeue a queued command.
2094
+ */
2095
+ sendKeys(data, paneId) {
2096
+ var _a;
2097
+ if (this.disconnected)
2098
+ return;
2099
+ const hex = data.toString('hex');
2100
+ if (hex.length > 0) {
2101
+ // Split into chunks to avoid command length limits
2102
+ for (let i = 0; i < hex.length; i += this.sendKeysChunkSize) {
2103
+ const chunk = hex.substring(i, i + this.sendKeysChunkSize);
2104
+ const hexBytes = ((_a = chunk.match(/.{2}/g)) === null || _a === void 0 ? void 0 : _a.join(' ')) || '';
2105
+ // Write directly — bypasses command queue for zero-latency input
2106
+ this.write(`send-keys -t %${paneId} -H ${hexBytes}\r`);
2107
+ this.directWritesPending++;
2108
+ }
2109
+ }
2110
+ }
2111
+ /**
2112
+ * Request detach
2113
+ */
2114
+ detach() {
2115
+ if (!this.detachSent) {
2116
+ this.write('detach\r');
2117
+ this.detachSent = true;
2118
+ }
2119
+ }
2120
+ /**
2121
+ * Feed raw PTY data. Buffers incomplete lines across calls so that TCP
2122
+ * fragment boundaries never split a protocol line.
2123
+ */
2124
+ executeData(data) {
2125
+ this.lineBuffer += data.toString('utf-8');
2126
+ let newlineIdx;
2127
+ while ((newlineIdx = this.lineBuffer.indexOf('\n')) !== -1) {
2128
+ const rawLine = this.lineBuffer.substring(0, newlineIdx);
2129
+ this.lineBuffer = this.lineBuffer.substring(newlineIdx + 1);
2130
+ const line = rawLine.replace(/\r$/, '');
2131
+ if (line) {
2132
+ this.executeLine(line);
2133
+ }
2134
+ }
2135
+ }
2136
+ /**
2137
+ * Process a single complete line from tmux control mode
2138
+ */
2139
+ executeLine(line) {
2140
+ // Strip DCS artifacts
2141
+ line = line.replace(/^\x1bP\d+p/, '').replace(/^P\d+p/, '').replace(/\x1b\\$/, '');
2142
+ if (!line)
2143
+ return;
2144
+ this.log.info(`Received: ${line.substring(0, 100)}${line.length > 100 ? '...' : ''}`);
2145
+ // Handle response blocks
2146
+ if (this.inResponseBlock) {
2147
+ if (line.startsWith(`%end ${this.currentCommandId}`) ||
2148
+ line.startsWith(`%end `)) {
2149
+ this.stripLastNewline();
2150
+ this.finishCurrentCommand(false);
2151
+ return;
2152
+ }
2153
+ else if (line.startsWith(`%error ${this.currentCommandId}`) ||
2154
+ line.startsWith(`%error `)) {
2155
+ this.stripLastNewline();
2156
+ this.finishCurrentCommand(true);
2157
+ return;
2158
+ }
2159
+ else if (line.startsWith('%exit')) {
2160
+ // Tmux 1.8 bug workaround
2161
+ this.stripLastNewline();
2162
+ this.finishCurrentCommand(false);
2163
+ // Fall through to handle %exit
2164
+ }
2165
+ else if (line.startsWith('%output ') || line.startsWith('%extended-output ')) {
2166
+ // Dispatch pane output notifications even during response blocks.
2167
+ // Otherwise they get accumulated as garbage text in the command
2168
+ // response (e.g. capture-pane) and the live output is lost.
2169
+ if (this.acceptNotifications) {
2170
+ if (line.startsWith('%output ')) {
2171
+ this.parseOutput(line);
2172
+ }
2173
+ else {
2174
+ this.parseExtendedOutput(line);
2175
+ }
2176
+ }
2177
+ return;
2178
+ }
2179
+ else {
2180
+ // Accumulate response
2181
+ this.currentResponse.push(line);
2182
+ return;
2183
+ }
2184
+ }
2185
+ // Handle notifications and commands
2186
+ if (line.startsWith('%begin')) {
2187
+ this.parseBegin(line);
2188
+ }
2189
+ else if (line.startsWith('%output ')) {
2190
+ if (this.acceptNotifications) {
2191
+ this.log.info(`Parsing output line: ${line.substring(0, 50)}...`);
2192
+ this.parseOutput(line);
2193
+ }
2194
+ else {
2195
+ this.log.warn(`Ignored output (not accepting notifications): ${line.substring(0, 50)}...`);
2196
+ }
2197
+ }
2198
+ else if (line.startsWith('%extended-output ')) {
2199
+ if (this.acceptNotifications) {
2200
+ this.parseExtendedOutput(line);
2201
+ }
2202
+ }
2203
+ else if (line.startsWith('%layout-change ')) {
2204
+ if (this.acceptNotifications) {
2205
+ this.parseLayoutChange(line);
2206
+ }
2207
+ }
2208
+ else if (line.startsWith('%window-add')) {
2209
+ if (this.acceptNotifications) {
2210
+ this.parseWindowAdd(line);
2211
+ }
2212
+ }
2213
+ else if (line.startsWith('%window-close') || line.startsWith('%unlinked-window-close')) {
2214
+ if (this.acceptNotifications) {
2215
+ this.parseWindowClose(line);
2216
+ }
2217
+ }
2218
+ else if (line.startsWith('%window-renamed') || line.startsWith('%unlinked-window-renamed')) {
2219
+ if (this.acceptNotifications) {
2220
+ this.parseWindowRenamed(line);
2221
+ }
2222
+ }
2223
+ else if (line.startsWith('%session-changed')) {
2224
+ this.parseSessionChanged(line);
2225
+ }
2226
+ else if (line.startsWith('%sessions-changed')) {
2227
+ if (this.acceptNotifications) {
2228
+ this.sessionsChanged$.next();
2229
+ }
2230
+ }
2231
+ else if (line.startsWith('%session-window-changed')) {
2232
+ if (this.acceptNotifications) {
2233
+ this.parseSessionWindowChanged(line);
2234
+ }
2235
+ }
2236
+ else if (line.startsWith('%window-pane-changed')) {
2237
+ if (this.acceptNotifications) {
2238
+ this.parsePaneChanged(line);
2239
+ }
2240
+ }
2241
+ else if (line.startsWith('%pane-close') || line.startsWith('%unlinked-pane-close')) {
2242
+ if (this.acceptNotifications) {
2243
+ this.parsePaneClose(line);
2244
+ }
2245
+ }
2246
+ else if (line.startsWith('%pause') || line.startsWith('%continue')) {
2247
+ // Flow control notifications (tmux 3.2+) — acknowledged
2248
+ this.log.debug(`Flow control: ${line}`);
2249
+ }
2250
+ else if (line.startsWith('%no-output')) {
2251
+ // Empty response block — no action needed
2252
+ }
2253
+ else if (line.startsWith('%exit')) {
2254
+ this.parseExit(line);
2255
+ }
2256
+ else if (line.startsWith('%')) {
2257
+ // Unknown notification, ignore
2258
+ this.log.debug(`Unknown notification: ${line}`);
2259
+ }
2260
+ }
2261
+ // --- Protocol Parsing ---
2262
+ parseBegin(line) {
2263
+ // %begin timestamp commandId flags
2264
+ // Format: %begin 1767853190 875 1
2265
+ // tmux Control Mode docs: flags is always 1 for client-originated.
2266
+ const parts = line.split(' ');
2267
+ if (parts.length < 3) {
2268
+ this.logger.warn(`Malformed %begin: ${line}`);
2269
+ return;
2270
+ }
2271
+ const commandId = parts[2];
2272
+ this.currentCommandId = commandId;
2273
+ this.currentResponse = [];
2274
+ this.inResponseBlock = true;
2275
+ // If this response is for a fire-and-forget write (sendKeys),
2276
+ // consume it without dequeuing a queued command.
2277
+ if (this.commandQueue.length === 0 && this.directWritesPending > 0) {
2278
+ this.directWritesPending--;
2279
+ this.currentCommand = null;
2280
+ return;
2281
+ }
2282
+ if (this.commandQueue.length === 0) {
2283
+ // Server-initiated or unexpected response block
2284
+ this.currentCommand = null;
2285
+ return;
2286
+ }
2287
+ this.currentCommand = this.commandQueue.shift();
2288
+ }
2289
+ finishCurrentCommand(isError) {
2290
+ this.inResponseBlock = false;
2291
+ const response = this.currentResponse.join('\n');
2292
+ if (this.currentCommand) {
2293
+ if (isError && !(this.currentCommand.flags & exports.TMUX_COMMAND_TOLERATE_ERRORS)) {
2294
+ this.currentCommand.reject(new Error(response));
2295
+ }
2296
+ else {
2297
+ this.currentCommand.resolve(response);
2298
+ }
2299
+ this.currentCommand = null;
2300
+ }
2301
+ // Mark as initialized after first successful response
2302
+ if (!this.initialized) {
2303
+ this.initialized = true;
2304
+ this.acceptNotifications = true;
2305
+ this.initialized$.next();
2306
+ }
2307
+ }
2308
+ stripLastNewline() {
2309
+ if (this.currentResponse.length > 0) {
2310
+ const last = this.currentResponse[this.currentResponse.length - 1];
2311
+ if (last === '') {
2312
+ this.currentResponse.pop();
2313
+ }
2314
+ }
2315
+ }
2316
+ parseOutput(line) {
2317
+ // %output %<pane> <data>
2318
+ const match = line.match(/^%output %(\d+) (.*)$/);
2319
+ if (!match) {
2320
+ this.logger.error(`Output regex FAILED for line: <${line}>`);
2321
+ return;
2322
+ }
2323
+ const paneId = parseInt(match[1]);
2324
+ const data = this.decodeOutput(match[2]);
2325
+ this.log.info(`Parsed output for pane %${paneId}: ${data.length} bytes (match_len=${match[2].length})`);
2326
+ this.output$.next({ paneId, data });
2327
+ }
2328
+ parseExtendedOutput(line) {
2329
+ // %extended-output %<pane> <latency> : <data>
2330
+ const match = line.match(/^%extended-output %(\d+) (\d+) : (.*)$/);
2331
+ if (!match)
2332
+ return;
2333
+ const paneId = parseInt(match[1]);
2334
+ const latency = parseInt(match[2]) / 1000; // Convert ms to seconds
2335
+ const data = this.decodeOutput(match[3]);
2336
+ this.output$.next({ paneId, data, latency });
2337
+ }
2338
+ parseLayoutChange(line) {
2339
+ // %layout-change @<window> <layout> [visible_layout flags]
2340
+ const match = line.match(/^%layout-change @(\d+) (.+)$/);
2341
+ if (!match)
2342
+ return;
2343
+ const windowId = parseInt(match[1]);
2344
+ const parts = match[2].split(' ');
2345
+ const layout = parts[0];
2346
+ const visibleLayout = parts.length > 1 ? parts[1] : undefined;
2347
+ const zoomed = parts.length > 2 ? parts[2].includes('Z') : undefined;
2348
+ this.layoutChange$.next({ windowId, layout, visibleLayout, zoomed });
2349
+ }
2350
+ parseWindowAdd(line) {
2351
+ // %window-add @<id>
2352
+ const match = line.match(/^%window-add @(\d+)$/);
2353
+ if (match) {
2354
+ this.windowAdd$.next(parseInt(match[1]));
2355
+ }
2356
+ }
2357
+ parseWindowClose(line) {
2358
+ // %window-close @<id> or %unlinked-window-close @<id>
2359
+ const match = line.match(/@(\d+)$/);
2360
+ if (match) {
2361
+ this.windowClose$.next(parseInt(match[1]));
2362
+ }
2363
+ }
2364
+ parseWindowRenamed(line) {
2365
+ // %window-renamed @<id> <name>
2366
+ const match = line.match(/^%(?:unlinked-)?window-renamed @(\d+) (.+)$/);
2367
+ if (match) {
2368
+ this.windowRenamed$.next({
2369
+ windowId: parseInt(match[1]),
2370
+ name: this.unescapeTmuxWindowName(match[2])
2371
+ });
2372
+ }
2373
+ }
2374
+ parseSessionChanged(line) {
2375
+ // %session-changed $<id> <name>
2376
+ const match = line.match(/^%session-changed \$(\d+) (.+)$/);
2377
+ if (match) {
2378
+ this.sessionChanged$.next({
2379
+ sessionId: parseInt(match[1]),
2380
+ sessionName: match[2]
2381
+ });
2382
+ // Enable notifications after session change
2383
+ this.acceptNotifications = true;
2384
+ }
2385
+ }
2386
+ parseSessionWindowChanged(line) {
2387
+ // %session-window-changed $session @window
2388
+ const match = line.match(/^%session-window-changed \$\d+ @(\d+)/);
2389
+ if (match) {
2390
+ this.sessionWindowChanged$.next({
2391
+ windowId: parseInt(match[1])
2392
+ });
2393
+ }
2394
+ }
2395
+ parsePaneChanged(line) {
2396
+ // %window-pane-changed @<window> %<pane>
2397
+ const match = line.match(/^%window-pane-changed @(\d+) %(\d+)$/);
2398
+ if (match) {
2399
+ this.paneChanged$.next({
2400
+ windowId: parseInt(match[1]),
2401
+ paneId: parseInt(match[2])
2402
+ });
2403
+ }
2404
+ }
2405
+ parsePaneClose(line) {
2406
+ // %pane-close @<window> %<pane>
2407
+ const match = line.match(/^%(?:unlinked-)?pane-close @(\d+) %(\d+)$/);
2408
+ if (match) {
2409
+ this.paneClose$.next({
2410
+ windowId: parseInt(match[1]),
2411
+ paneId: parseInt(match[2])
2412
+ });
2413
+ }
2414
+ }
2415
+ parseExit(line) {
2416
+ // %exit or %exit <reason>
2417
+ const reason = line.replace(/^%exit\s*/, '');
2418
+ this.exit$.next(reason);
2419
+ this.disconnected = true;
2420
+ }
2421
+ // --- Utility Methods ---
2422
+ write(data) {
2423
+ this.writer(data);
2424
+ }
2425
+ /**
2426
+ * Decode tmux octal-escaped output to Buffer
2427
+ */
2428
+ decodeOutput(str) {
2429
+ const bytes = [];
2430
+ for (let i = 0; i < str.length; i++) {
2431
+ if (str[i] === '\\' && i + 3 < str.length) {
2432
+ const octal = str.substring(i + 1, i + 4);
2433
+ if (/^[0-7]{3}$/.test(octal)) {
2434
+ bytes.push(parseInt(octal, 8));
2435
+ i += 3;
2436
+ continue;
2437
+ }
2438
+ }
2439
+ // Handle UTF-8 properly
2440
+ const buf = Buffer.from(str[i], 'utf-8');
2441
+ for (const byte of buf) {
2442
+ bytes.push(byte);
2443
+ }
2444
+ }
2445
+ return Buffer.from(bytes);
2446
+ }
2447
+ unescapeTmuxWindowName(name) {
2448
+ // Tmux may escape window names
2449
+ return name.replace(/\\(.)/g, '$1');
2450
+ }
2451
+ /**
2452
+ * Check if server version is at least the given version
2453
+ */
2454
+ versionAtLeast(version) {
2455
+ return this.minimumServerVersion !== null && this.minimumServerVersion >= version;
2456
+ }
2457
+ }
2458
+ exports.TmuxGateway = TmuxGateway;
2459
+
2460
+
2461
+ /***/ },
2462
+
2463
+ /***/ "./src/index.ts"
2464
+ /*!**********************!*\
2465
+ !*** ./src/index.ts ***!
2466
+ \**********************/
2467
+ (__unused_webpack_module, exports, __webpack_require__) {
2468
+
2469
+ "use strict";
2470
+
2471
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
2472
+ if (k2 === undefined) k2 = k;
2473
+ var desc = Object.getOwnPropertyDescriptor(m, k);
2474
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
2475
+ desc = { enumerable: true, get: function() { return m[k]; } };
2476
+ }
2477
+ Object.defineProperty(o, k2, desc);
2478
+ }) : (function(o, m, k, k2) {
2479
+ if (k2 === undefined) k2 = k;
2480
+ o[k2] = m[k];
2481
+ }));
2482
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
2483
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
2484
+ }) : function(o, v) {
2485
+ o["default"] = v;
2486
+ });
2487
+ var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
2488
+ var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
2489
+ if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
2490
+ else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
2491
+ return c > 3 && r && Object.defineProperty(target, key, r), r;
2492
+ };
2493
+ var __importStar = (this && this.__importStar) || function (mod) {
2494
+ if (mod && mod.__esModule) return mod;
2495
+ var result = {};
2496
+ if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
2497
+ __setModuleDefault(result, mod);
2498
+ return result;
2499
+ };
2500
+ Object.defineProperty(exports, "__esModule", ({ value: true }));
2501
+ const core_1 = __webpack_require__(/*! @angular/core */ "@angular/core");
2502
+ const common_1 = __webpack_require__(/*! @angular/common */ "@angular/common");
2503
+ const forms_1 = __webpack_require__(/*! @angular/forms */ "@angular/forms");
2504
+ const tabby_core_1 = __importStar(__webpack_require__(/*! tabby-core */ "tabby-core"));
2505
+ const tabby_settings_1 = __webpack_require__(/*! tabby-settings */ "tabby-settings");
2506
+ const tabContextMenu_1 = __webpack_require__(/*! ./tabContextMenu */ "./src/tabContextMenu.ts");
2507
+ const config_1 = __webpack_require__(/*! ./config */ "./src/config.ts");
2508
+ const settings_1 = __webpack_require__(/*! ./settings */ "./src/settings.ts");
2509
+ const tmuxPaneTab_component_1 = __webpack_require__(/*! ./components/tmuxPaneTab.component */ "./src/components/tmuxPaneTab.component.ts");
2510
+ const tmuxSessionTab_component_1 = __webpack_require__(/*! ./components/tmuxSessionTab.component */ "./src/components/tmuxSessionTab.component.ts");
2511
+ const tmuxWindowBar_component_1 = __webpack_require__(/*! ./components/tmuxWindowBar.component */ "./src/components/tmuxWindowBar.component.ts");
2512
+ const settings_component_1 = __webpack_require__(/*! ./components/settings.component */ "./src/components/settings.component.ts");
2513
+ let TmuxModule = class TmuxModule {
2514
+ };
2515
+ TmuxModule = __decorate([
2516
+ (0, core_1.NgModule)({
2517
+ imports: [
2518
+ common_1.CommonModule,
2519
+ forms_1.FormsModule,
2520
+ tabby_core_1.default,
2521
+ ],
2522
+ providers: [
2523
+ { provide: tabby_core_1.TabContextMenuItemProvider, useClass: tabContextMenu_1.TmuxContextMenuProvider, multi: true },
2524
+ { provide: tabby_core_1.ConfigProvider, useClass: config_1.TmuxConfigProvider, multi: true },
2525
+ { provide: tabby_settings_1.SettingsTabProvider, useClass: settings_1.TmuxSettingsTabProvider, multi: true },
2526
+ ],
2527
+ declarations: [
2528
+ tmuxPaneTab_component_1.TmuxPaneTabComponent,
2529
+ tmuxSessionTab_component_1.TmuxSessionTabComponent,
2530
+ tmuxWindowBar_component_1.TmuxWindowBarComponent,
2531
+ settings_component_1.TmuxSettingsTabComponent,
2532
+ ],
2533
+ entryComponents: [
2534
+ tmuxPaneTab_component_1.TmuxPaneTabComponent,
2535
+ tmuxSessionTab_component_1.TmuxSessionTabComponent,
2536
+ settings_component_1.TmuxSettingsTabComponent,
2537
+ ],
2538
+ })
2539
+ ], TmuxModule);
2540
+ exports["default"] = TmuxModule;
2541
+
2542
+
2543
+ /***/ },
2544
+
2545
+ /***/ "./src/layoutParser.ts"
2546
+ /*!*****************************!*\
2547
+ !*** ./src/layoutParser.ts ***!
2548
+ \*****************************/
2549
+ (__unused_webpack_module, exports) {
2550
+
2551
+ "use strict";
2552
+
2553
+ /**
2554
+ * TmuxLayoutParser - Parse tmux layout strings to extract pane positions
2555
+ *
2556
+ * Tmux layout string format:
2557
+ * - Checksum,WxH,X,Y<content>
2558
+ * - Content can be:
2559
+ * - Just a pane ID: "93x52,0,0,185"
2560
+ * - Vertical split [...]: contains comma-separated panes stacked top-to-bottom
2561
+ * - Horizontal split {...}: contains comma-separated panes arranged left-to-right
2562
+ *
2563
+ * Example: "41e9,279x71,0,0[279x40,0,0,71,279x30,0,41{147x30,0,41,72,131x30,148,41,73}]"
2564
+ */
2565
+ Object.defineProperty(exports, "__esModule", ({ value: true }));
2566
+ exports.layoutToSplitFormat = exports.flattenLayout = exports.parseTmuxLayout = void 0;
2567
+ /**
2568
+ * Parse a tmux layout string into a tree structure
2569
+ */
2570
+ function parseTmuxLayout(layoutStr) {
2571
+ if (!layoutStr)
2572
+ return null;
2573
+ // Remove checksum if present (4 hex chars followed by comma)
2574
+ const checksumMatch = layoutStr.match(/^[0-9a-f]{4},/);
2575
+ if (checksumMatch) {
2576
+ layoutStr = layoutStr.substring(5);
2577
+ }
2578
+ try {
2579
+ return parseNode(layoutStr, 0).node;
2580
+ }
2581
+ catch (e) {
2582
+ console.error('Failed to parse tmux layout:', e);
2583
+ return null;
2584
+ }
2585
+ }
2586
+ exports.parseTmuxLayout = parseTmuxLayout;
2587
+ /**
2588
+ * Parse a single node from the layout string
2589
+ */
2590
+ function parseNode(str, start) {
2591
+ // Parse dimension: WxH,X,Y
2592
+ const dimMatch = str.substring(start).match(/^(\d+)x(\d+),(\d+),(\d+)/);
2593
+ if (!dimMatch) {
2594
+ throw new Error(`Invalid dimension at position ${start}: ${str.substring(start, start + 20)}`);
2595
+ }
2596
+ const width = parseInt(dimMatch[1]);
2597
+ const height = parseInt(dimMatch[2]);
2598
+ const x = parseInt(dimMatch[3]);
2599
+ const y = parseInt(dimMatch[4]);
2600
+ let pos = start + dimMatch[0].length;
2601
+ // Check what follows
2602
+ if (pos >= str.length) {
2603
+ // End of string - this shouldn't happen for a complete layout
2604
+ throw new Error('Unexpected end of layout string');
2605
+ }
2606
+ const nextChar = str[pos];
2607
+ if (nextChar === '[') {
2608
+ // Vertical split (top-to-bottom) — tmux [...] = panes stacked vertically
2609
+ pos++; // skip '['
2610
+ const children = [];
2611
+ while (str[pos] !== ']') {
2612
+ const result = parseNode(str, pos);
2613
+ children.push(result.node);
2614
+ pos = result.consumed;
2615
+ if (str[pos] === ',') {
2616
+ pos++; // skip comma between children
2617
+ }
2618
+ }
2619
+ pos++; // skip ']'
2620
+ return {
2621
+ node: { type: 'vertical', x, y, width, height, children },
2622
+ consumed: pos
2623
+ };
2624
+ }
2625
+ else if (nextChar === '{') {
2626
+ // Horizontal split (left-to-right) — tmux {...} = panes side by side
2627
+ pos++; // skip '{'
2628
+ const children = [];
2629
+ while (str[pos] !== '}') {
2630
+ const result = parseNode(str, pos);
2631
+ children.push(result.node);
2632
+ pos = result.consumed;
2633
+ if (str[pos] === ',') {
2634
+ pos++; // skip comma between children
2635
+ }
2636
+ }
2637
+ pos++; // skip '}'
2638
+ return {
2639
+ node: { type: 'horizontal', x, y, width, height, children },
2640
+ consumed: pos
2641
+ };
2642
+ }
2643
+ else if (nextChar === ',' || nextChar === ']' || nextChar === '}' || pos >= str.length) {
2644
+ // This is a pane - the next number is the pane ID
2645
+ // But wait, we need to check if there's a pane ID
2646
+ // The format is WxH,X,Y,PaneID for a simple pane
2647
+ if (nextChar === ',') {
2648
+ pos++; // skip comma
2649
+ const paneIdMatch = str.substring(pos).match(/^(\d+)/);
2650
+ if (paneIdMatch) {
2651
+ const paneId = parseInt(paneIdMatch[1]);
2652
+ pos += paneIdMatch[0].length;
2653
+ return {
2654
+ node: { type: 'pane', x, y, width, height, paneId },
2655
+ consumed: pos
2656
+ };
2657
+ }
2658
+ }
2659
+ // No pane ID found, this might be a container
2660
+ return {
2661
+ node: { type: 'pane', x, y, width, height },
2662
+ consumed: pos
2663
+ };
2664
+ }
2665
+ else {
2666
+ throw new Error(`Unexpected character '${nextChar}' at position ${pos}`);
2667
+ }
2668
+ }
2669
+ /**
2670
+ * Flatten a layout tree into a list of panes
2671
+ */
2672
+ function flattenLayout(node) {
2673
+ const panes = [];
2674
+ function traverse(n) {
2675
+ if (n.type === 'pane' && n.paneId !== undefined) {
2676
+ panes.push({
2677
+ paneId: n.paneId,
2678
+ x: n.x,
2679
+ y: n.y,
2680
+ width: n.width,
2681
+ height: n.height
2682
+ });
2683
+ }
2684
+ if (n.children) {
2685
+ for (const child of n.children) {
2686
+ traverse(child);
2687
+ }
2688
+ }
2689
+ }
2690
+ traverse(node);
2691
+ return panes;
2692
+ }
2693
+ exports.flattenLayout = flattenLayout;
2694
+ function layoutToSplitFormat(node) {
2695
+ var _a;
2696
+ if (node.type === 'pane') {
2697
+ return (_a = node.paneId) !== null && _a !== void 0 ? _a : null;
2698
+ }
2699
+ if (!node.children || node.children.length === 0) {
2700
+ return null;
2701
+ }
2702
+ const orientation = node.type === 'horizontal' ? 'horizontal' : 'vertical';
2703
+ // Calculate ratios based on dimensions
2704
+ const totalSize = orientation === 'horizontal'
2705
+ ? node.children.reduce((sum, c) => sum + c.width, 0)
2706
+ : node.children.reduce((sum, c) => sum + c.height, 0);
2707
+ const ratios = node.children.map(c => orientation === 'horizontal'
2708
+ ? c.width / totalSize
2709
+ : c.height / totalSize);
2710
+ const children = node.children.map(c => layoutToSplitFormat(c)).filter(c => c !== null);
2711
+ return { orientation, ratios, children: children };
2712
+ }
2713
+ exports.layoutToSplitFormat = layoutToSplitFormat;
2714
+
2715
+
2716
+ /***/ },
2717
+
2718
+ /***/ "./src/logHelper.ts"
2719
+ /*!**************************!*\
2720
+ !*** ./src/logHelper.ts ***!
2721
+ \**************************/
2722
+ (__unused_webpack_module, exports) {
2723
+
2724
+ "use strict";
2725
+
2726
+ Object.defineProperty(exports, "__esModule", ({ value: true }));
2727
+ exports.createConditionalLogger = void 0;
2728
+ /**
2729
+ * Wrap a Logger so that debug/info are gated behind debugLogging config,
2730
+ * while warn/error always pass through.
2731
+ */
2732
+ function createConditionalLogger(logger, configService) {
2733
+ return {
2734
+ debug: (...args) => {
2735
+ var _a, _b;
2736
+ if ((_b = (_a = configService === null || configService === void 0 ? void 0 : configService.store) === null || _a === void 0 ? void 0 : _a.tmuxPlugin) === null || _b === void 0 ? void 0 : _b.debugLogging)
2737
+ logger.debug(...args);
2738
+ },
2739
+ info: (...args) => {
2740
+ var _a, _b;
2741
+ if ((_b = (_a = configService === null || configService === void 0 ? void 0 : configService.store) === null || _a === void 0 ? void 0 : _a.tmuxPlugin) === null || _b === void 0 ? void 0 : _b.debugLogging)
2742
+ logger.info(...args);
2743
+ },
2744
+ warn: logger.warn.bind(logger),
2745
+ error: logger.error.bind(logger),
2746
+ };
2747
+ }
2748
+ exports.createConditionalLogger = createConditionalLogger;
2749
+
2750
+
2751
+ /***/ },
2752
+
2753
+ /***/ "./src/services/tmux.service.ts"
2754
+ /*!**************************************!*\
2755
+ !*** ./src/services/tmux.service.ts ***!
2756
+ \**************************************/
2757
+ (__unused_webpack_module, exports, __webpack_require__) {
2758
+
2759
+ "use strict";
2760
+
2761
+ var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
2762
+ var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
2763
+ if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
2764
+ else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
2765
+ return c > 3 && r && Object.defineProperty(target, key, r), r;
2766
+ };
2767
+ var __metadata = (this && this.__metadata) || function (k, v) {
2768
+ if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
2769
+ };
2770
+ Object.defineProperty(exports, "__esModule", ({ value: true }));
2771
+ exports.TmuxService = exports.TmuxOutputInterceptor = void 0;
2772
+ const core_1 = __webpack_require__(/*! @angular/core */ "@angular/core");
2773
+ const tabby_core_1 = __webpack_require__(/*! tabby-core */ "tabby-core");
2774
+ const logHelper_1 = __webpack_require__(/*! ../logHelper */ "./src/logHelper.ts");
2775
+ const tabby_terminal_1 = __webpack_require__(/*! tabby-terminal */ "tabby-terminal");
2776
+ const rxjs_1 = __webpack_require__(/*! rxjs */ "rxjs");
2777
+ const session_1 = __webpack_require__(/*! ../session */ "./src/session.ts");
2778
+ const tmuxSessionTab_component_1 = __webpack_require__(/*! ../components/tmuxSessionTab.component */ "./src/components/tmuxSessionTab.component.ts");
2779
+ /**
2780
+ * Middleware inserted at position 0 of the original session's middleware chain
2781
+ * when entering tmux mode. It captures raw output data for the tmux gateway
2782
+ * and BLOCKS it from propagating further, so that other middleware plugins
2783
+ * (e.g. trzsz) on the original session do not see tmux control mode data.
2784
+ *
2785
+ * Without this interceptor, trzsz middleware on the original session would
2786
+ * detect trzsz protocol markers embedded in %output lines and trigger
2787
+ * chooseSendFiles() a second time ("double file dialog" bug).
2788
+ */
2789
+ class TmuxOutputInterceptor extends tabby_terminal_1.SessionMiddleware {
2790
+ constructor() {
2791
+ super(...arguments);
2792
+ this._rawOutput = new rxjs_1.Subject();
2793
+ /** Raw session output, before any middleware processing */
2794
+ this.rawOutput$ = this._rawOutput.asObservable();
2795
+ // feedFromTerminal is NOT overridden — terminal→session data flows
2796
+ // through normally so the session can still receive input.
2797
+ }
2798
+ feedFromSession(data) {
2799
+ // Capture raw data for the tmux gateway
2800
+ this._rawOutput.next(data);
2801
+ // Do NOT call super.feedFromSession() — this blocks data from
2802
+ // propagating to the rest of the middleware chain (trzsz, etc.)
2803
+ }
2804
+ }
2805
+ exports.TmuxOutputInterceptor = TmuxOutputInterceptor;
2806
+ let TmuxService = class TmuxService {
2807
+ constructor(injector, appService, configService, log) {
2808
+ this.injector = injector;
2809
+ this.appService = appService;
2810
+ this.configService = configService;
2811
+ this.sessions = new Set();
2812
+ this.logger = log.create('tmux-service');
2813
+ }
2814
+ get log() {
2815
+ return (0, logHelper_1.createConditionalLogger)(this.logger, this.configService);
2816
+ }
2817
+ get isConnected() {
2818
+ return this.sessions.size > 0;
2819
+ }
2820
+ get controller() {
2821
+ var _a;
2822
+ return ((_a = this.sessions.values().next().value) === null || _a === void 0 ? void 0 : _a.controller) || null;
2823
+ }
2824
+ /**
2825
+ * Find the SessionContext that owns a given sessionTab.
2826
+ */
2827
+ findContextForTab(tab) {
2828
+ for (const ctx of this.sessions) {
2829
+ if (ctx.sessionTab === tab)
2830
+ return ctx;
2831
+ }
2832
+ return undefined;
2833
+ }
2834
+ setupControllerEvents(context) {
2835
+ context.subscriptions.push(context.controller.events.subscribe(event => {
2836
+ // On initialized, replace the terminal tab with the session tab
2837
+ if (event.type === 'initialized' && !context.sessionTab) {
2838
+ this.replaceWithSessionTab(context);
2839
+ }
2840
+ }));
2841
+ }
2842
+ replaceWithSessionTab(context) {
2843
+ if (context.sessionTab)
2844
+ return;
2845
+ this.log.info('Creating TmuxSessionTab...');
2846
+ // Find the topmost parent tab (the actual tab listed in the top tab bar)
2847
+ const topmostTab = context.terminalTab.topmostParent || context.terminalTab;
2848
+ context.topmostTab = topmostTab;
2849
+ // Remember the original index so we can replace in-place
2850
+ const tabs = this.appService.tabs;
2851
+ const index = tabs.indexOf(topmostTab);
2852
+ context.topmostTabIndex = index;
2853
+ // IMPORTANT: We must use openNewTabRaw, NOT openNewTab.
2854
+ // openNewTab wraps non-SplitTab types in a wrapper SplitTab via wrapAndAddTab().
2855
+ // But TmuxSessionTabComponent extends SplitTabComponent, and wrapAndAddTab's
2856
+ // SplitTab.addTab(thing) has special logic: when thing instanceof SplitTabComponent,
2857
+ // it extracts thing.root and then DESTROYS thing. This kills our component instance
2858
+ // before it ever gets rendered, so ngOnInit/ngAfterViewInit never fire.
2859
+ //
2860
+ // openNewTabRaw adds the tab directly without wrapping, so our component's
2861
+ // view is properly attached and lifecycle hooks execute normally.
2862
+ const sessionTab = this.appService.openNewTabRaw({
2863
+ type: tmuxSessionTab_component_1.TmuxSessionTabComponent,
2864
+ inputs: {
2865
+ existingController: context.controller,
2866
+ profile: { sessionName: context.controller.getSessionName() },
2867
+ },
2868
+ });
2869
+ context.sessionTab = sessionTab;
2870
+ // Move the session tab to the same position as the original tab
2871
+ if (index !== -1) {
2872
+ const sessionIndex = tabs.indexOf(sessionTab);
2873
+ if (sessionIndex !== -1) {
2874
+ tabs.splice(sessionIndex, 1); // remove from end
2875
+ tabs.splice(index, 0, sessionTab) // insert at original position
2876
+ ;
2877
+ this.appService.tabsChanged.next();
2878
+ }
2879
+ }
2880
+ // Hide the original topmost tab
2881
+ if (index !== -1) {
2882
+ const origIndex = tabs.indexOf(topmostTab);
2883
+ if (origIndex !== -1) {
2884
+ tabs.splice(origIndex, 1);
2885
+ this.appService.tabsChanged.next();
2886
+ }
2887
+ }
2888
+ // When the session tab is closed (by user or disconnect), clean up
2889
+ context.subscriptions.push(sessionTab.destroyed$.subscribe(() => {
2890
+ context.sessionTab = undefined;
2891
+ }));
2892
+ }
2893
+ async disconnectContext(context) {
2894
+ var _a;
2895
+ this.sessions.delete(context);
2896
+ context.subscriptions.forEach(s => s.unsubscribe());
2897
+ // Detach from tmux control mode so the tmux client process exits
2898
+ // cleanly. Without this, the original terminal tab's PTY still has
2899
+ // a running `tmux -CC attach` process, causing "tmux is still running"
2900
+ // confirmation dialogs when the user tries to close the restored tab.
2901
+ context.controller.gateway.detach();
2902
+ // Remove the output interceptor from the original session's middleware chain
2903
+ if (context.outputInterceptor) {
2904
+ (_a = context.terminalTab.session) === null || _a === void 0 ? void 0 : _a.middleware.remove(context.outputInterceptor);
2905
+ context.outputInterceptor = undefined;
2906
+ }
2907
+ await context.controller.destroy();
2908
+ // Destroy the session tab (removes from tab bar)
2909
+ if (context.sessionTab) {
2910
+ context.sessionTab.destroy();
2911
+ context.sessionTab = undefined;
2912
+ }
2913
+ // Restore the original topmost tab to the tab bar at its original position
2914
+ if (context.topmostTab) {
2915
+ const tabs = this.appService.tabs;
2916
+ const insertAt = context.topmostTabIndex !== undefined
2917
+ ? Math.min(context.topmostTabIndex, tabs.length)
2918
+ : tabs.length;
2919
+ tabs.splice(insertAt, 0, context.topmostTab);
2920
+ this.appService.tabsChanged.next();
2921
+ this.appService.selectTab(context.topmostTab);
2922
+ }
2923
+ this.log.info('Disconnected tmux context');
2924
+ }
2925
+ /**
2926
+ * Disconnect from all sessions
2927
+ */
2928
+ async disconnect() {
2929
+ for (const context of this.sessions) {
2930
+ await this.disconnectContext(context);
2931
+ }
2932
+ }
2933
+ /**
2934
+ * Attach to tmux from an existing terminal tab.
2935
+ * Replaces the terminal tab with a TmuxSessionTab, keeping the terminal tab
2936
+ * hidden in context. On disconnect, the terminal tab is restored.
2937
+ */
2938
+ async attachToTerminal(terminalTab) {
2939
+ var _a, _b;
2940
+ const session = terminalTab.session;
2941
+ if (!session) {
2942
+ this.logger.error('Terminal tab has no session');
2943
+ return;
2944
+ }
2945
+ this.log.info('Attaching tmux to existing terminal session');
2946
+ const context = {
2947
+ controller: null,
2948
+ terminalTab,
2949
+ subscriptions: []
2950
+ };
2951
+ // Insert a tmux output interceptor at position 0 of the session's
2952
+ // middleware chain. This captures raw output for the gateway and
2953
+ // prevents trzsz (or other) middleware on the original session from
2954
+ // seeing tmux control mode data (which would cause false positives).
2955
+ const interceptor = new TmuxOutputInterceptor();
2956
+ session.middleware.unshift(interceptor);
2957
+ context.outputInterceptor = interceptor;
2958
+ // Create a controller that uses the terminal's session for I/O
2959
+ context.controller = new session_1.TmuxController(this.logger, this.injector, (data) => session.write(Buffer.from(data)), () => this.disconnectContext(context), this.configService);
2960
+ // Subscribe to the interceptor's raw output to parse tmux control mode.
2961
+ // Feed raw buffers directly — the gateway handles line buffering internally
2962
+ // via executeData(), which properly handles TCP fragment boundaries.
2963
+ context.subscriptions.push(interceptor.rawOutput$.subscribe((data) => {
2964
+ context.controller.handleData(data);
2965
+ }));
2966
+ // Handle terminal tab closure (disconnect on close)
2967
+ context.subscriptions.push(terminalTab.destroyed$.subscribe(() => {
2968
+ this.log.info('Attached terminal tab closed, disconnecting session');
2969
+ this.disconnectContext(context);
2970
+ }));
2971
+ this.sessions.add(context);
2972
+ this.setupControllerEvents(context);
2973
+ // Send the tmux -CC command to the terminal
2974
+ const sessionName = (_b = (_a = this.configService.store.tmuxPlugin) === null || _a === void 0 ? void 0 : _a.defaultSessionName) !== null && _b !== void 0 ? _b : 'default';
2975
+ session.write(Buffer.from(`tmux -CC new -A -s ${sessionName}\n`));
2976
+ }
2977
+ };
2978
+ TmuxService.ctorParameters = () => [
2979
+ { type: core_1.Injector },
2980
+ { type: tabby_core_1.AppService },
2981
+ { type: tabby_core_1.ConfigService },
2982
+ { type: tabby_core_1.LogService }
2983
+ ];
2984
+ TmuxService = __decorate([
2985
+ (0, core_1.Injectable)({ providedIn: 'root' }),
2986
+ __metadata("design:paramtypes", [core_1.Injector,
2987
+ tabby_core_1.AppService,
2988
+ tabby_core_1.ConfigService,
2989
+ tabby_core_1.LogService])
2990
+ ], TmuxService);
2991
+ exports.TmuxService = TmuxService;
2992
+
2993
+
2994
+ /***/ },
2995
+
2996
+ /***/ "./src/session.ts"
2997
+ /*!************************!*\
2998
+ !*** ./src/session.ts ***!
2999
+ \************************/
3000
+ (__unused_webpack_module, exports, __webpack_require__) {
3001
+
3002
+ "use strict";
3003
+
3004
+ Object.defineProperty(exports, "__esModule", ({ value: true }));
3005
+ exports.TmuxControllerSession = exports.TmuxController = exports.TmuxPaneSession = void 0;
3006
+ const rxjs_1 = __webpack_require__(/*! rxjs */ "rxjs");
3007
+ const tabby_terminal_1 = __webpack_require__(/*! tabby-terminal */ "tabby-terminal");
3008
+ const logHelper_1 = __webpack_require__(/*! ./logHelper */ "./src/logHelper.ts");
3009
+ const gateway_1 = __webpack_require__(/*! ./gateway */ "./src/gateway.ts");
3010
+ /**
3011
+ * TmuxPaneSession - Represents a single tmux pane as a terminal session
3012
+ */
3013
+ class TmuxPaneSession extends tabby_terminal_1.BaseSession {
3014
+ constructor(logger, controller, paneId) {
3015
+ super(logger);
3016
+ this.controller = controller;
3017
+ this.paneId = paneId;
3018
+ /**
3019
+ * Saved alternate screen content + cursor position, persisted after
3020
+ * restorePaneHistory so that xterm.resize() (from setTmuxGrid after
3021
+ * %layout-change) can re-apply it. xterm.resize() clears the
3022
+ * alternate screen buffer, so the content must be written again.
3023
+ */
3024
+ this.pendingAltRestore = null;
3025
+ /**
3026
+ * Incomplete screen-title sequence (ESC k ... ESC \) spanning
3027
+ * multiple feedOutput calls. Buffered until the closing ST arrives.
3028
+ */
3029
+ this._pendingTitleSeq = null;
3030
+ this.open = true;
3031
+ this.controller.registerPane(this.paneId, this);
3032
+ }
3033
+ async start() {
3034
+ this.open = true;
3035
+ // Restore history — for initial attach this is instant (pre-loaded
3036
+ // during batch discovery); for runtime panes it falls back to
3037
+ // capture-pane.
3038
+ await this.controller.restorePaneHistory(this.paneId);
3039
+ }
3040
+ resize(_columns, _rows) {
3041
+ // No-op by design. In tmux integration, tmux is authoritative over the
3042
+ // cell grid: each pane's character size comes from the %layout-change
3043
+ // string and is applied via TmuxPaneTabComponent.setTmuxGrid().
3044
+ //
3045
+ // The xterm frontend's automatic fit-to-container resizing is disabled
3046
+ // for tmux panes (frontend.enableResizing = false), so this method
3047
+ // should normally never be called. Sending refresh-client -C from here
3048
+ // would re-introduce the resize feedback loop (pane refit → client
3049
+ // size → tmux relayout → pane refit → ...), so we deliberately do
3050
+ // nothing. Overall client size is driven only by the container size
3051
+ // in TmuxSessionTabComponent.refreshClientSize().
3052
+ }
3053
+ write(data) {
3054
+ this.controller.writeToPane(this.paneId, data);
3055
+ }
3056
+ // NOTE: feedFromTerminal is NOT overridden — it goes through the
3057
+ // middleware chain (BaseSession.feedFromTerminal → middleware →
3058
+ // outputToSession$ → write()) so that SessionMiddleware plugins
3059
+ // such as trzsz can intercept terminal input.
3060
+ kill(_signal) {
3061
+ this.destroy();
3062
+ }
3063
+ async destroy() {
3064
+ this.pendingAltRestore = null;
3065
+ this._pendingTitleSeq = null;
3066
+ await super.destroy();
3067
+ this.controller.unregisterPane(this.paneId);
3068
+ }
3069
+ async gracefullyKillProcess() {
3070
+ this.destroy();
3071
+ }
3072
+ supportsWorkingDirectory() {
3073
+ return false;
3074
+ }
3075
+ async getWorkingDirectory() {
3076
+ return null;
3077
+ }
3078
+ /**
3079
+ * Public wrapper for the protected emitOutput().
3080
+ * Used by TmuxController to deliver history and buffered output.
3081
+ *
3082
+ * Filters out screen/tmux "set window title" sequences (ESC k ... ESC \)
3083
+ * which xterm.js does not recognize. Without filtering, zsh precmd/preexec
3084
+ * hooks that set the terminal title via `print -Pn "\ek%s\e\\"` would leak
3085
+ * the title text as visible output (e.g. `echo111` instead of `111`).
3086
+ */
3087
+ feedOutput(data) {
3088
+ data = this.filterScreenTitleSequences(data);
3089
+ if (data.length > 0) {
3090
+ this.emitOutput(data);
3091
+ }
3092
+ }
3093
+ /**
3094
+ * Strip screen/tmux "set window title" sequences (ESC k ... ESC \)
3095
+ * from the output stream.
3096
+ *
3097
+ * In screen/tmux, `ESC k <title> ESC \` sets the window/tab title.
3098
+ * tmux processes these internally but also forwards them verbatim to
3099
+ * control-mode clients. xterm.js does NOT handle this sequence — it
3100
+ * only recognizes `ESC ] ... BEL/ST` (OSC) — so the title text leaks
3101
+ * as visible content (e.g. the command name appears before output).
3102
+ *
3103
+ * Handles sequences that span multiple feedOutput calls by buffering
3104
+ * the incomplete portion until the closing ST (ESC \) arrives.
3105
+ */
3106
+ filterScreenTitleSequences(data) {
3107
+ // Prepend leftover from previous call
3108
+ if (this._pendingTitleSeq) {
3109
+ data = Buffer.concat([this._pendingTitleSeq, data]);
3110
+ this._pendingTitleSeq = null;
3111
+ }
3112
+ const ESC = 0x1b;
3113
+ const parts = [];
3114
+ let pos = 0;
3115
+ while (pos < data.length) {
3116
+ // Find next ESC k (0x1b 0x6b)
3117
+ let startIdx = -1;
3118
+ for (let i = pos; i < data.length - 1; i++) {
3119
+ if (data[i] === ESC && data[i + 1] === 0x6b) {
3120
+ startIdx = i;
3121
+ break;
3122
+ }
3123
+ }
3124
+ if (startIdx < 0) {
3125
+ // No more title sequences — emit the rest.
3126
+ // Buffer a trailing ESC (0x1b) in case the next call
3127
+ // starts with 0x6b ('k'), forming a split ESC k pair.
3128
+ const tail = data[data.length - 1];
3129
+ if (tail === ESC) {
3130
+ parts.push(data.subarray(pos, data.length - 1));
3131
+ this._pendingTitleSeq = data.subarray(data.length - 1);
3132
+ }
3133
+ else {
3134
+ parts.push(data.subarray(pos));
3135
+ }
3136
+ break;
3137
+ }
3138
+ // Emit data before the title sequence
3139
+ if (startIdx > pos) {
3140
+ parts.push(data.subarray(pos, startIdx));
3141
+ }
3142
+ // Search for ESC \ (ST: 0x1b 0x5c) after ESC k
3143
+ let stIdx = -1;
3144
+ for (let i = startIdx + 2; i < data.length - 1; i++) {
3145
+ if (data[i] === ESC && data[i + 1] === 0x5c) {
3146
+ stIdx = i;
3147
+ break;
3148
+ }
3149
+ }
3150
+ if (stIdx >= 0) {
3151
+ // Complete sequence found — skip it entirely
3152
+ pos = stIdx + 2;
3153
+ }
3154
+ else {
3155
+ // Incomplete sequence — buffer from ESC k onwards
3156
+ this._pendingTitleSeq = data.subarray(startIdx);
3157
+ break;
3158
+ }
3159
+ }
3160
+ if (parts.length === 0)
3161
+ return Buffer.alloc(0);
3162
+ if (parts.length === 1)
3163
+ return parts[0];
3164
+ return Buffer.concat(parts);
3165
+ }
3166
+ }
3167
+ exports.TmuxPaneSession = TmuxPaneSession;
3168
+ /**
3169
+ * TmuxController - Manages a tmux control mode session
3170
+ *
3171
+ * Based on iTerm2's TmuxController architecture.
3172
+ */
3173
+ class TmuxController {
3174
+ get log() {
3175
+ return (0, logHelper_1.createConditionalLogger)(this.logger, this.configService);
3176
+ }
3177
+ constructor(logger, _injector, // eslint-disable-line @typescript-eslint/no-unused-vars
3178
+ writer, closer, configService) {
3179
+ this.logger = logger;
3180
+ this.closer = closer;
3181
+ this.configService = configService;
3182
+ this.paneSessions = new Map();
3183
+ this.windowStates = new Map();
3184
+ this.knownPanes = new Set();
3185
+ this.pendingPaneOutput = new Map();
3186
+ /** Pre-loaded history from batch discovery (iTerm2-style). */
3187
+ this.pendingSnapshots = new Map();
3188
+ this.sessionName = '';
3189
+ this.sessionId = -1;
3190
+ this.attached = false;
3191
+ this.activeWindowId = null;
3192
+ this.events = new rxjs_1.Subject();
3193
+ this.gateway = new gateway_1.TmuxGateway(logger, writer, configService);
3194
+ this.setupGatewaySubscriptions();
3195
+ }
3196
+ setupGatewaySubscriptions() {
3197
+ // Handle pane output
3198
+ this.gateway.output$.subscribe(({ paneId, data }) => {
3199
+ this.log.info(`Session received output for pane %${paneId}: ${data.length} bytes`);
3200
+ if (this.paneSessions.has(paneId)) {
3201
+ this.paneSessions.get(paneId).feedOutput(data);
3202
+ }
3203
+ else {
3204
+ // Buffer output for panes not yet registered
3205
+ if (!this.pendingPaneOutput.has(paneId)) {
3206
+ this.pendingPaneOutput.set(paneId, []);
3207
+ }
3208
+ this.pendingPaneOutput.get(paneId).push(data);
3209
+ }
3210
+ });
3211
+ // Handle session changes - this is our main initialization point
3212
+ // Like iTerm2, we immediately batch-discover all windows and panes
3213
+ // instead of relying on delayed list-panes or passive %output discovery.
3214
+ this.gateway.sessionChanged$.subscribe(({ sessionName, sessionId }) => {
3215
+ this.sessionName = sessionName;
3216
+ this.sessionId = sessionId;
3217
+ this.attached = true;
3218
+ this.log.info(`Attached to session: ${sessionName} ($${sessionId})`);
3219
+ this.events.next({ type: 'session-changed', data: { sessionName, sessionId } });
3220
+ // Immediate batch discovery — no setTimeout delay
3221
+ this.discoverWindowsAndPanes();
3222
+ });
3223
+ // Handle window events
3224
+ this.gateway.windowAdd$.subscribe(windowId => {
3225
+ if (!this.windowStates.has(windowId)) {
3226
+ this.windowStates.set(windowId, {
3227
+ id: windowId,
3228
+ name: `Window ${windowId}`,
3229
+ panes: new Set()
3230
+ });
3231
+ }
3232
+ this.events.next({ type: 'window-add', windowId });
3233
+ // For new windows created at runtime (after initial attach),
3234
+ // tmux may NOT send %layout-change — only %window-add and %output.
3235
+ // We must proactively discover the window's layout and panes.
3236
+ // The window-add event has already been emitted above, so the UI
3237
+ // has registered the window. discoverWindowsAndPanes will update
3238
+ // the windowState with layout and emit pane-add + layout-change.
3239
+ this.discoverWindowsAndPanes();
3240
+ });
3241
+ this.gateway.windowClose$.subscribe(windowId => {
3242
+ this.windowStates.delete(windowId);
3243
+ this.events.next({ type: 'window-close', windowId });
3244
+ });
3245
+ this.gateway.windowRenamed$.subscribe(({ windowId, name }) => {
3246
+ const state = this.windowStates.get(windowId);
3247
+ if (state) {
3248
+ state.name = name;
3249
+ }
3250
+ this.events.next({ type: 'window-renamed', windowId, data: { name } });
3251
+ });
3252
+ // Handle pane close events (tmux 3.2+)
3253
+ this.gateway.paneClose$.subscribe(({ windowId, paneId }) => {
3254
+ this.log.info(`Pane %${paneId} closed in window @${windowId}`);
3255
+ // Remove from known panes
3256
+ this.knownPanes.delete(paneId);
3257
+ // Remove from window state
3258
+ const windowState = this.windowStates.get(windowId);
3259
+ if (windowState) {
3260
+ windowState.panes.delete(paneId);
3261
+ }
3262
+ // Clean up pane session if exists
3263
+ const session = this.paneSessions.get(paneId);
3264
+ if (session) {
3265
+ session.destroy();
3266
+ this.paneSessions.delete(paneId);
3267
+ }
3268
+ this.pendingPaneOutput.delete(paneId);
3269
+ this.events.next({ type: 'pane-close', paneId, windowId });
3270
+ });
3271
+ // Handle layout changes — primary pane discovery trigger (iTerm2-style).
3272
+ // Layout strings contain pane IDs. We extract new panes, capture their
3273
+ // history/state, then emit pane-add events so the UI can create tabs
3274
+ // with pre-loaded data. This replaces the old refreshPanes()-based
3275
+ // discovery for runtime pane creation (split-window etc.).
3276
+ //
3277
+ // IMPORTANT: We do NOT emit 'layout-change' here. discoverPanesFromLayout
3278
+ // emits it after pane-add events, ensuring syncLayout() always runs
3279
+ // after pane tabs have been created.
3280
+ this.gateway.layoutChange$.subscribe(({ windowId, layout, visibleLayout, zoomed }) => {
3281
+ const state = this.windowStates.get(windowId);
3282
+ if (state) {
3283
+ state.layout = layout;
3284
+ }
3285
+ // Discover new panes from the layout string, then emit layout-change
3286
+ this.discoverPanesFromLayout(windowId, layout, visibleLayout, zoomed);
3287
+ });
3288
+ // Handle exit
3289
+ // Handle session-window-changed — the current window changed
3290
+ this.gateway.sessionWindowChanged$.subscribe(({ windowId }) => {
3291
+ this.log.info(`Active window changed to @${windowId}`);
3292
+ this.activeWindowId = windowId;
3293
+ this.events.next({ type: 'active-window-changed', windowId });
3294
+ });
3295
+ this.gateway.exit$.subscribe(reason => {
3296
+ this.attached = false;
3297
+ this.events.next({ type: 'exit', data: { reason } });
3298
+ this.closer();
3299
+ });
3300
+ // Handle initialization
3301
+ this.gateway.initialized$.subscribe(() => {
3302
+ this.events.next({ type: 'initialized' });
3303
+ this.discoverWindowsAndPanes();
3304
+ });
3305
+ }
3306
+ /**
3307
+ * Process a line from the underlying session
3308
+ */
3309
+ handleLine(line) {
3310
+ this.gateway.executeLine(line);
3311
+ }
3312
+ /**
3313
+ * Feed raw PTY data to the gateway for byte-level DCS buffering.
3314
+ * Preferred over handleLine for proper handling of TCP fragments.
3315
+ */
3316
+ handleData(data) {
3317
+ this.gateway.executeData(data);
3318
+ }
3319
+ /**
3320
+ * Batch-discover all windows, panes, and history (iTerm2-style).
3321
+ *
3322
+ * Sequence (mirrors TmuxWindowOpener):
3323
+ * 1. list-windows → discover windows with names + layout
3324
+ * 2. list-panes → discover pane IDs
3325
+ * 3. capture-pane for each new pane → pre-load history
3326
+ * 4. emit pane-add events (history already in pendingHistory)
3327
+ *
3328
+ * By the time the UI creates a TmuxPaneTabComponent for a pane,
3329
+ * its history is already captured — no async restore or buffering
3330
+ * is needed at the session level.
3331
+ */
3332
+ async discoverWindowsAndPanes() {
3333
+ this.log.info('Batch discovering windows and panes...');
3334
+ try {
3335
+ // Step 1: Discover all windows with names, layout and active flag
3336
+ const winResult = await this.gateway.sendCommand('list-windows -F "#{window_id} #{window_name} #{window_active} #{window_layout}"', gateway_1.TMUX_COMMAND_TOLERATE_ERRORS);
3337
+ const winLines = winResult.split(/[\r\n]+/).map(l => l.trim()).filter(l => l);
3338
+ this.log.info(`Found ${winLines.length} window(s) from list-windows`);
3339
+ for (const line of winLines) {
3340
+ // Format: "@0 mywindow 1 1234,0x0,0,0{60x24,0,0,1}"
3341
+ const match = line.match(/^@?(\d+)\s+(.+?)\s+([01])\s+(.+)$/);
3342
+ if (match) {
3343
+ const windowId = parseInt(match[1]);
3344
+ const windowName = match[2];
3345
+ const active = match[3] === '1';
3346
+ const layout = match[4];
3347
+ if (active) {
3348
+ this.activeWindowId = windowId;
3349
+ }
3350
+ if (!this.windowStates.has(windowId)) {
3351
+ this.windowStates.set(windowId, {
3352
+ id: windowId,
3353
+ name: windowName,
3354
+ layout,
3355
+ panes: new Set()
3356
+ });
3357
+ this.events.next({ type: 'window-add', windowId });
3358
+ }
3359
+ else {
3360
+ const state = this.windowStates.get(windowId);
3361
+ state.name = windowName;
3362
+ state.layout = layout;
3363
+ }
3364
+ }
3365
+ }
3366
+ // Step 2: Discover all panes and map to windows
3367
+ const paneResult = await this.gateway.sendCommand('list-panes -s -F "#{pane_id} #{window_id}"', gateway_1.TMUX_COMMAND_TOLERATE_ERRORS);
3368
+ const paneLines = paneResult.split(/[\r\n]+/).map(l => l.trim()).filter(l => l);
3369
+ this.log.info(`Found ${paneLines.length} pane(s) from list-panes`);
3370
+ const newPaneIds = [];
3371
+ for (const line of paneLines) {
3372
+ const match = line.match(/^%?(\d+)\s+@?(\d+)$/);
3373
+ if (match) {
3374
+ const paneId = parseInt(match[1]);
3375
+ const windowId = parseInt(match[2]);
3376
+ let windowState = this.windowStates.get(windowId);
3377
+ if (!windowState) {
3378
+ windowState = {
3379
+ id: windowId,
3380
+ name: `Window ${windowId}`,
3381
+ panes: new Set()
3382
+ };
3383
+ this.windowStates.set(windowId, windowState);
3384
+ this.events.next({ type: 'window-add', windowId });
3385
+ }
3386
+ windowState.panes.add(paneId);
3387
+ if (!this.knownPanes.has(paneId)) {
3388
+ this.knownPanes.add(paneId);
3389
+ newPaneIds.push({ paneId, windowId });
3390
+ }
3391
+ }
3392
+ }
3393
+ // Step 3: Batch-capture history + state for all new panes
3394
+ // (mirrors iTerm2 TmuxWindowOpener)
3395
+ if (newPaneIds.length > 0) {
3396
+ this.log.info(`Capturing history/state for ${newPaneIds.length} new pane(s)...`);
3397
+ await this.capturePaneSnapshots(newPaneIds);
3398
+ }
3399
+ // Step 4: Emit pane-add events — history is now pre-loaded
3400
+ for (const { paneId, windowId } of newPaneIds) {
3401
+ this.log.info(`Discovered pane %${paneId} in window @${windowId}`);
3402
+ this.events.next({ type: 'pane-add', paneId, windowId });
3403
+ }
3404
+ // Step 5: Emit layout-change for all discovered windows so the UI
3405
+ // can build the SplitContainer tree. Without this, syncLayout()
3406
+ // never runs and panes remain registered but unmounted.
3407
+ for (const windowState of this.windowStates.values()) {
3408
+ if (windowState.layout) {
3409
+ this.events.next({
3410
+ type: 'layout-change',
3411
+ windowId: windowState.id,
3412
+ data: { layout: windowState.layout },
3413
+ });
3414
+ }
3415
+ }
3416
+ }
3417
+ catch (e) {
3418
+ this.logger.warn('Failed to batch discover windows/panes:', e);
3419
+ }
3420
+ }
3421
+ /**
3422
+ * Public alias for discoverWindowsAndPanes.
3423
+ * Used by external callers (context menu, session tab ngAfterViewInit)
3424
+ * to trigger a full re-scan. For runtime pane creation (split-window),
3425
+ * discoverPanesFromLayout() handles it via %layout-change instead.
3426
+ */
3427
+ async refreshPanes() {
3428
+ return this.discoverWindowsAndPanes();
3429
+ }
3430
+ /**
3431
+ * Discover new panes from a %layout-change notification (iTerm2-style).
3432
+ *
3433
+ * When tmux sends a layout change, the layout string contains all pane
3434
+ * IDs for that window. We compare against known panes, capture
3435
+ * history/state for any new ones, emit pane-add events, then emit
3436
+ * layout-change so syncLayout() runs after pane tabs exist.
3437
+ */
3438
+ async discoverPanesFromLayout(windowId, layout, visibleLayout, zoomed) {
3439
+ // Extract pane IDs from the layout string
3440
+ // Pane IDs appear as trailing numbers in layout leaf nodes like "80x24,0,0,3"
3441
+ // Match pattern: dimension,position,paneId (paneId is the last number after the last comma)
3442
+ const paneIdSet = new Set();
3443
+ // Match all occurrences of ,\d+ at the end of leaf node specs
3444
+ const leafPattern = /\d+x\d+,\d+,\d+,(\d+)/g;
3445
+ let m;
3446
+ while ((m = leafPattern.exec(layout)) !== null) {
3447
+ paneIdSet.add(parseInt(m[1]));
3448
+ }
3449
+ if (paneIdSet.size === 0)
3450
+ return;
3451
+ const windowState = this.windowStates.get(windowId);
3452
+ const newPaneIds = [];
3453
+ for (const paneId of paneIdSet) {
3454
+ if (windowState) {
3455
+ windowState.panes.add(paneId);
3456
+ }
3457
+ if (!this.knownPanes.has(paneId)) {
3458
+ this.knownPanes.add(paneId);
3459
+ newPaneIds.push({ paneId, windowId });
3460
+ }
3461
+ }
3462
+ if (newPaneIds.length > 0) {
3463
+ this.log.info(`Discovered ${newPaneIds.length} new pane(s) from layout-change for window @${windowId}`);
3464
+ // Capture history + state for new panes (same as discoverWindowsAndPanes Step 3)
3465
+ await this.capturePaneSnapshots(newPaneIds);
3466
+ // Emit pane-add events — history is now pre-loaded
3467
+ for (const { paneId, windowId: wid } of newPaneIds) {
3468
+ this.events.next({ type: 'pane-add', paneId, windowId: wid });
3469
+ }
3470
+ }
3471
+ // Emit layout-change AFTER pane-add events, so syncLayout() can
3472
+ // create views for newly discovered panes. This ordering is critical:
3473
+ // pane-add → handlePaneAdd (creates pane tab) → layout-change →
3474
+ // syncLayout (attaches view + builds SplitTree).
3475
+ this.events.next({ type: 'layout-change', windowId, data: { layout, visibleLayout, zoomed } });
3476
+ }
3477
+ /**
3478
+ * Capture history + state for an array of panes.
3479
+ * Shared by discoverWindowsAndPanes() and discoverPanesFromLayout().
3480
+ */
3481
+ async capturePaneSnapshots(paneIds) {
3482
+ const stateFormat = [
3483
+ 'pane_id=#{pane_id}',
3484
+ 'alternate_on=#{alternate_on}',
3485
+ 'alternate_saved_x=#{alternate_saved_x}',
3486
+ 'alternate_saved_y=#{alternate_saved_y}',
3487
+ 'cursor_x=#{cursor_x}',
3488
+ 'cursor_y=#{cursor_y}',
3489
+ 'scroll_region_upper=#{scroll_region_upper}',
3490
+ 'scroll_region_lower=#{scroll_region_lower}',
3491
+ 'pane_tabs=#{pane_tabs}',
3492
+ 'cursor_flag=#{cursor_flag}',
3493
+ 'insert_flag=#{insert_flag}',
3494
+ 'keypad_cursor_flag=#{keypad_cursor_flag}',
3495
+ 'keypad_flag=#{keypad_flag}',
3496
+ 'wrap_flag=#{wrap_flag}',
3497
+ 'bracket_paste_flag=#{bracket_paste_flag}',
3498
+ 'mouse_standard_flag=#{mouse_standard_flag}',
3499
+ 'mouse_button_flag=#{mouse_button_flag}',
3500
+ 'mouse_any_flag=#{mouse_any_flag}',
3501
+ ].join('\t');
3502
+ const captures = paneIds.map(async ({ paneId }) => {
3503
+ try {
3504
+ const [history, altHistory, stateResult] = await Promise.all([
3505
+ this.gateway.sendCommand(`capture-pane -peqJN -S- -t %${paneId}`, gateway_1.TMUX_COMMAND_TOLERATE_ERRORS),
3506
+ this.gateway.sendCommand(`capture-pane -peqJN -a -S- -t %${paneId}`, gateway_1.TMUX_COMMAND_TOLERATE_ERRORS),
3507
+ this.gateway.sendCommand(`list-panes -t %${paneId} -F "${stateFormat}"`, gateway_1.TMUX_COMMAND_TOLERATE_ERRORS),
3508
+ ]);
3509
+ const state = this.parsePaneState(stateResult, paneId);
3510
+ this.pendingSnapshots.set(paneId, { history, altHistory, state });
3511
+ }
3512
+ catch (e) {
3513
+ this.logger.warn(`Failed to capture snapshot for pane %${paneId}:`, e);
3514
+ }
3515
+ });
3516
+ await Promise.all(captures);
3517
+ }
3518
+ // --- Pane Management ---
3519
+ registerPane(paneId, session) {
3520
+ this.paneSessions.set(paneId, session);
3521
+ this.knownPanes.add(paneId);
3522
+ // Flush any buffered output that arrived before registration
3523
+ const buffered = this.pendingPaneOutput.get(paneId);
3524
+ if (buffered) {
3525
+ for (const data of buffered) {
3526
+ session.feedOutput(data);
3527
+ }
3528
+ this.pendingPaneOutput.delete(paneId);
3529
+ }
3530
+ }
3531
+ unregisterPane(paneId) {
3532
+ this.paneSessions.delete(paneId);
3533
+ this.pendingPaneOutput.delete(paneId);
3534
+ this.pendingSnapshots.delete(paneId);
3535
+ }
3536
+ getPaneSession(paneId) {
3537
+ return this.paneSessions.get(paneId);
3538
+ }
3539
+ hasPaneSession(paneId) {
3540
+ return this.paneSessions.has(paneId);
3541
+ }
3542
+ resizePane(_paneId, columns, rows) {
3543
+ // Use refresh-client -C to set client size
3544
+ // This affects all panes uniformly in non-variable-size mode
3545
+ // Note: paneId is ignored as tmux control mode uses uniform size
3546
+ this.gateway.sendCommand(`refresh-client -C ${columns},${rows}`, gateway_1.TMUX_COMMAND_TOLERATE_ERRORS).catch(e => this.logger.warn('Resize failed:', e));
3547
+ }
3548
+ writeToPane(paneId, data) {
3549
+ this.log.info(`Writing ${data.length} bytes to pane %${paneId}: <${data.toString('hex')}>`);
3550
+ this.gateway.sendKeys(data, paneId);
3551
+ }
3552
+ /**
3553
+ * Restore pane history.
3554
+ *
3555
+ * History + state are pre-loaded during discoverWindowsAndPanes()
3556
+ * (stored in pendingSnapshots) — this is instant, no capture-pane needed.
3557
+ * Both initial attach and runtime panes (split-window etc.) go through
3558
+ * discoverWindowsAndPanes() before pane-add events are emitted, so
3559
+ * pendingSnapshots is always populated by the time this runs.
3560
+ *
3561
+ * Restores (like iTerm2 setTmuxHistory:altHistory:state:):
3562
+ * 1. Primary screen history
3563
+ * 2. Alternate screen history (via CSI ?1047h / escape sequences)
3564
+ * 3. Terminal state (cursor, scroll region, modes)
3565
+ */
3566
+ async restorePaneHistory(paneId) {
3567
+ const snapshot = this.pendingSnapshots.get(paneId);
3568
+ if (!snapshot) {
3569
+ this.logger.warn(`No pre-loaded snapshot for pane %${paneId}, skipping`);
3570
+ return;
3571
+ }
3572
+ this.pendingSnapshots.delete(paneId);
3573
+ const session = this.paneSessions.get(paneId);
3574
+ if (!session)
3575
+ return;
3576
+ const state = snapshot.state;
3577
+ // Step 1: Write primary screen history to the primary screen.
3578
+ // This sets up the scrollback so it's available if the user leaves
3579
+ // the program running on the alternate screen.
3580
+ if (snapshot.history) {
3581
+ const normalized = snapshot.history.replace(/\n/g, '\r\n');
3582
+ session.feedOutput(Buffer.from(normalized, 'utf-8'));
3583
+ }
3584
+ // Step 2: If the pane is on the alternate screen (vim, less, etc.),
3585
+ // switch to it and write the alternate content. We stay on alternate.
3586
+ if (state.alternateOn) {
3587
+ // ?1047h enters alternate screen and clears it
3588
+ session.feedOutput(Buffer.from('\x1b[?1047h', 'utf-8'));
3589
+ // Apply terminal state on the alternate screen (scroll region,
3590
+ // modes, cursor visibility — NOT the cursor position yet).
3591
+ this.applyPaneState(session, state);
3592
+ // Write the alternate screen content at the top-left corner.
3593
+ // capture-pane with -a gives us exactly what was on the alternate
3594
+ // screen, starting from row 0.
3595
+ if (snapshot.altHistory && snapshot.altHistory.trim()) {
3596
+ session.feedOutput(Buffer.from('\x1b[H', 'utf-8'));
3597
+ const normalized = snapshot.altHistory.replace(/\n/g, '\r\n');
3598
+ session.feedOutput(Buffer.from(normalized, 'utf-8'));
3599
+ }
3600
+ // Re-apply cursor position after content write (content may
3601
+ // have moved the cursor via embedded CUP sequences).
3602
+ const csi = (s) => `\x1b[${s}`;
3603
+ session.feedOutput(Buffer.from(csi(`${state.cursorY + 1};${state.cursorX + 1}H`), 'utf-8'));
3604
+ // Save alternate screen data for re-apply after xterm.resize()
3605
+ // (called by setTmuxGrid). xterm.resize() clears the alternate
3606
+ // screen buffer, so the content must be written again.
3607
+ session.pendingAltRestore = {
3608
+ content: snapshot.altHistory || '',
3609
+ cursorY: state.cursorY,
3610
+ cursorX: state.cursorX,
3611
+ modes: this.buildModeSequences(state),
3612
+ };
3613
+ }
3614
+ else {
3615
+ // Normal mode — write alternate history if present (rare)
3616
+ if (snapshot.altHistory && snapshot.altHistory.trim()) {
3617
+ session.feedOutput(Buffer.from('\x1b[?1047h', 'utf-8'));
3618
+ const normalized = snapshot.altHistory.replace(/\n/g, '\r\n');
3619
+ session.feedOutput(Buffer.from(normalized, 'utf-8'));
3620
+ session.feedOutput(Buffer.from('\x1b[?1047l', 'utf-8'));
3621
+ }
3622
+ // Apply terminal state (cursor, scroll region, modes)
3623
+ this.applyPaneState(session, state);
3624
+ }
3625
+ }
3626
+ /**
3627
+ * Parse pane state from `list-panes -F` response.
3628
+ * Mirrors iTerm2 TmuxStateParser.
3629
+ */
3630
+ parsePaneState(response, expectedPaneId) {
3631
+ const state = {
3632
+ paneId: expectedPaneId,
3633
+ cursorX: 0, cursorY: 0,
3634
+ alternateOn: false,
3635
+ alternateSavedX: 0, alternateSavedY: 0,
3636
+ scrollRegionUpper: 0, scrollRegionLower: 0,
3637
+ wrapFlag: true, cursorFlag: true,
3638
+ insertFlag: false, bracketPasteFlag: false,
3639
+ keypadCursorFlag: false, keypadFlag: false,
3640
+ paneTabs: [],
3641
+ mouseStandardMode: false,
3642
+ mouseButtonMode: false,
3643
+ mouseAnyMode: false,
3644
+ };
3645
+ // `list-panes -t %paneId -F ...` may return multiple lines (one per pane
3646
+ // in the window) or a single tab-separated line. We must find the
3647
+ // segment whose pane_id matches expectedPaneId — the first match is NOT
3648
+ // necessarily the one we asked for.
3649
+ const lines = response.split(/[\r\n]+/);
3650
+ let targetLine = '';
3651
+ for (const line of lines) {
3652
+ if (!line.includes('pane_id='))
3653
+ continue;
3654
+ // Check if this line's pane_id matches our expected pane
3655
+ const idMatch = line.match(/pane_id=%?(\d+)/);
3656
+ if (idMatch && parseInt(idMatch[1]) === expectedPaneId) {
3657
+ targetLine = line;
3658
+ break;
3659
+ }
3660
+ }
3661
+ // Fallback: if no exact match found, use the first line with pane_id
3662
+ // (happens when list-panes returns only the target pane)
3663
+ if (!targetLine) {
3664
+ targetLine = lines.find(l => l.includes('pane_id=')) || '';
3665
+ }
3666
+ if (!targetLine)
3667
+ return state;
3668
+ for (const part of targetLine.split('\t')) {
3669
+ const eqIdx = part.indexOf('=');
3670
+ if (eqIdx < 0)
3671
+ continue;
3672
+ const key = part.substring(0, eqIdx);
3673
+ const value = part.substring(eqIdx + 1);
3674
+ const n = parseInt(value);
3675
+ switch (key) {
3676
+ case 'pane_id':
3677
+ state.paneId = n;
3678
+ break;
3679
+ case 'cursor_x':
3680
+ state.cursorX = n;
3681
+ break;
3682
+ case 'cursor_y':
3683
+ state.cursorY = n;
3684
+ break;
3685
+ case 'alternate_on':
3686
+ state.alternateOn = n === 1;
3687
+ break;
3688
+ case 'alternate_saved_x':
3689
+ state.alternateSavedX = n;
3690
+ break;
3691
+ case 'alternate_saved_y':
3692
+ state.alternateSavedY = n;
3693
+ break;
3694
+ case 'scroll_region_upper':
3695
+ state.scrollRegionUpper = n;
3696
+ break;
3697
+ case 'scroll_region_lower':
3698
+ state.scrollRegionLower = n;
3699
+ break;
3700
+ case 'pane_tabs':
3701
+ state.paneTabs = value.split(',').map(Number).filter(x => !isNaN(x));
3702
+ break;
3703
+ case 'cursor_flag':
3704
+ state.cursorFlag = n === 1;
3705
+ break;
3706
+ case 'insert_flag':
3707
+ state.insertFlag = n === 1;
3708
+ break;
3709
+ case 'keypad_cursor_flag':
3710
+ state.keypadCursorFlag = n === 1;
3711
+ break;
3712
+ case 'keypad_flag':
3713
+ state.keypadFlag = n === 1;
3714
+ break;
3715
+ case 'wrap_flag':
3716
+ state.wrapFlag = n === 1;
3717
+ break;
3718
+ case 'bracket_paste_flag':
3719
+ state.bracketPasteFlag = n === 1;
3720
+ break;
3721
+ case 'mouse_standard_flag':
3722
+ state.mouseStandardMode = n === 1;
3723
+ break;
3724
+ case 'mouse_button_flag':
3725
+ state.mouseButtonMode = n === 1;
3726
+ break;
3727
+ case 'mouse_any_flag':
3728
+ state.mouseAnyMode = n === 1;
3729
+ break;
3730
+ }
3731
+ }
3732
+ return state;
3733
+ }
3734
+ /**
3735
+ * Apply parsed pane state to the terminal via ANSI escape sequences.
3736
+ * Mirrors iTerm2 VT100ScreenMutableState.setTmuxState:.
3737
+ */
3738
+ applyPaneState(session, state) {
3739
+ // Build a sequence of escape codes to restore terminal state.
3740
+ const seq = this.buildModeSequences(state);
3741
+ session.feedOutput(Buffer.from(seq, 'utf-8'));
3742
+ }
3743
+ /**
3744
+ * Build ANSI escape sequences for terminal mode state (without alternate
3745
+ * screen entry). Used by both applyPaneState and pendingAltRestore.
3746
+ */
3747
+ buildModeSequences(state) {
3748
+ const csi = (s) => `\x1b[${s}`;
3749
+ const esc = (s) => `\x1b${s}`;
3750
+ let seq = '';
3751
+ // Set scroll region (DECSTBM)
3752
+ if (state.scrollRegionUpper > 0 || state.scrollRegionLower > 0) {
3753
+ seq += csi(`${state.scrollRegionUpper + 1};${state.scrollRegionLower + 1}r`);
3754
+ }
3755
+ // Restore cursor position (CUP)
3756
+ seq += csi(`${state.cursorY + 1};${state.cursorX + 1}H`);
3757
+ // Cursor visibility (DECTCEM)
3758
+ seq += state.cursorFlag ? csi('?25h') : csi('?25l');
3759
+ // Insert mode (IRM)
3760
+ seq += state.insertFlag ? csi('4h') : csi('4l');
3761
+ // Application cursor keys (DECCKM)
3762
+ seq += state.keypadCursorFlag ? csi('?1h') : csi('?1l');
3763
+ // Application keypad mode (DECKPAM / DECKPNM)
3764
+ seq += state.keypadFlag ? esc('=') : esc('>');
3765
+ // Bracketed paste mode
3766
+ seq += state.bracketPasteFlag ? csi('?2004h') : csi('?2004l');
3767
+ // Wrap mode (DECAWM)
3768
+ seq += state.wrapFlag ? csi('?7h') : csi('?7l');
3769
+ // Mouse tracking modes (?1000=normal, ?1002=button, ?1003=any)
3770
+ seq += state.mouseStandardMode ? csi('?1000h') : csi('?1000l');
3771
+ seq += state.mouseButtonMode ? csi('?1002h') : csi('?1002l');
3772
+ seq += state.mouseAnyMode ? csi('?1003h') : csi('?1003l');
3773
+ // Tab stops (HTS / TBC)
3774
+ // TBC 3 = clear all tab stops, then HTS at each position
3775
+ seq += csi('3g');
3776
+ for (const col of state.paneTabs) {
3777
+ seq += csi(`${col + 1}G`); // CUP to column
3778
+ seq += esc('H'); // HTS
3779
+ }
3780
+ // Reset cursor back to final position (tab stop setup moves it)
3781
+ seq += csi(`${state.cursorY + 1};${state.cursorX + 1}H`);
3782
+ return seq;
3783
+ }
3784
+ /**
3785
+ * Re-apply alternate screen content after xterm.resize() clears it.
3786
+ * Called by TmuxPaneTabComponent.applyTmuxGrid() after resize.
3787
+ */
3788
+ reapplyAltContent(session) {
3789
+ const alt = session.pendingAltRestore;
3790
+ if (!alt)
3791
+ return;
3792
+ // Clear immediately — this is a one-shot re-apply after the initial
3793
+ // resize. After this, live tmux output maintains the alternate screen.
3794
+ session.pendingAltRestore = null;
3795
+ // Enter alternate screen (clears it)
3796
+ session.feedOutput(Buffer.from('\x1b[?1047h', 'utf-8'));
3797
+ // Apply modes
3798
+ session.feedOutput(Buffer.from(alt.modes, 'utf-8'));
3799
+ // Write content at top-left
3800
+ if (alt.content && alt.content.trim()) {
3801
+ session.feedOutput(Buffer.from('\x1b[H', 'utf-8'));
3802
+ const normalized = alt.content.replace(/\n/g, '\r\n');
3803
+ session.feedOutput(Buffer.from(normalized, 'utf-8'));
3804
+ }
3805
+ // Re-apply cursor position
3806
+ const csi = (s) => `\x1b[${s}`;
3807
+ session.feedOutput(Buffer.from(csi(`${alt.cursorY + 1};${alt.cursorX + 1}H`), 'utf-8'));
3808
+ }
3809
+ async killPane(paneId) {
3810
+ await this.gateway.sendCommand(`kill-pane -t %${paneId}`, gateway_1.TMUX_COMMAND_TOLERATE_ERRORS);
3811
+ }
3812
+ // --- Window Operations ---
3813
+ async createWindow() {
3814
+ try {
3815
+ const result = await this.gateway.sendCommand('new-window -P -F "#{window_id}"');
3816
+ const match = result.match(/@(\d+)/);
3817
+ return match ? parseInt(match[1]) : null;
3818
+ }
3819
+ catch (e) {
3820
+ this.logger.warn('Failed to create window:', e);
3821
+ return null;
3822
+ }
3823
+ }
3824
+ async killWindow(windowId) {
3825
+ await this.gateway.sendCommand(`kill-window -t @${windowId}`, gateway_1.TMUX_COMMAND_TOLERATE_ERRORS);
3826
+ }
3827
+ async renameWindow(windowId, name) {
3828
+ await this.gateway.sendCommand(`rename-window -t @${windowId} "${name.replace(/"/g, '\\"')}"`, gateway_1.TMUX_COMMAND_TOLERATE_ERRORS);
3829
+ }
3830
+ // --- Session Operations ---
3831
+ async detach() {
3832
+ this.gateway.detach();
3833
+ }
3834
+ async listSessions() {
3835
+ try {
3836
+ const result = await this.gateway.sendCommand('list-sessions -F "#{session_id} #{session_name}"');
3837
+ const sessions = [];
3838
+ for (const line of result.split('\n')) {
3839
+ const match = line.match(/^\$(\d+) (.+)$/);
3840
+ if (match) {
3841
+ sessions.push({
3842
+ id: parseInt(match[1]),
3843
+ name: match[2]
3844
+ });
3845
+ }
3846
+ }
3847
+ return sessions;
3848
+ }
3849
+ catch (e) {
3850
+ this.logger.warn('Failed to list sessions:', e);
3851
+ return [];
3852
+ }
3853
+ }
3854
+ // --- Lifecycle ---
3855
+ async destroy() {
3856
+ // Close all pane sessions
3857
+ for (const [_paneId, session] of this.paneSessions) {
3858
+ await session.destroy();
3859
+ }
3860
+ this.paneSessions.clear();
3861
+ this.attached = false;
3862
+ }
3863
+ // --- Getters ---
3864
+ get isAttached() {
3865
+ return this.attached;
3866
+ }
3867
+ getSessionName() {
3868
+ return this.sessionName;
3869
+ }
3870
+ getSessionId() {
3871
+ return this.sessionId;
3872
+ }
3873
+ getWindowState(windowId) {
3874
+ return this.windowStates.get(windowId);
3875
+ }
3876
+ getAllWindowStates() {
3877
+ return Array.from(this.windowStates.values());
3878
+ }
3879
+ getFirstWindowId() {
3880
+ const first = this.windowStates.keys().next();
3881
+ return first.done ? undefined : first.value;
3882
+ }
3883
+ /**
3884
+ * Get the tmux-side active window ID, as reported by list-windows
3885
+ * #{window_active} or %session-window-changed. Falls back to null.
3886
+ */
3887
+ getActiveWindowId() {
3888
+ return this.activeWindowId;
3889
+ }
3890
+ /**
3891
+ * Get all known pane IDs across all windows.
3892
+ * Used by TmuxPaneTabComponent for "Focus all tmux panes" (sync input).
3893
+ */
3894
+ getAllPaneIds() {
3895
+ return Array.from(this.knownPanes);
3896
+ }
3897
+ }
3898
+ exports.TmuxController = TmuxController;
3899
+ exports.TmuxControllerSession = TmuxController;
3900
+
3901
+
3902
+ /***/ },
3903
+
3904
+ /***/ "./src/settings.ts"
3905
+ /*!*************************!*\
3906
+ !*** ./src/settings.ts ***!
3907
+ \*************************/
3908
+ (__unused_webpack_module, exports, __webpack_require__) {
3909
+
3910
+ "use strict";
3911
+
3912
+ var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
3913
+ var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
3914
+ if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
3915
+ else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
3916
+ return c > 3 && r && Object.defineProperty(target, key, r), r;
3917
+ };
3918
+ Object.defineProperty(exports, "__esModule", ({ value: true }));
3919
+ exports.TmuxSettingsTabProvider = void 0;
3920
+ const core_1 = __webpack_require__(/*! @angular/core */ "@angular/core");
3921
+ const tabby_settings_1 = __webpack_require__(/*! tabby-settings */ "tabby-settings");
3922
+ const settings_component_1 = __webpack_require__(/*! ./components/settings.component */ "./src/components/settings.component.ts");
3923
+ // eslint-disable-next-line new-cap
3924
+ let TmuxSettingsTabProvider = class TmuxSettingsTabProvider extends tabby_settings_1.SettingsTabProvider {
3925
+ constructor() {
3926
+ super(...arguments);
3927
+ this.id = 'tmux';
3928
+ this.icon = 'border-all';
3929
+ this.title = 'Tmux';
3930
+ }
3931
+ getComponentType() {
3932
+ return settings_component_1.TmuxSettingsTabComponent;
3933
+ }
3934
+ };
3935
+ TmuxSettingsTabProvider = __decorate([
3936
+ (0, core_1.Injectable)()
3937
+ ], TmuxSettingsTabProvider);
3938
+ exports.TmuxSettingsTabProvider = TmuxSettingsTabProvider;
3939
+
3940
+
3941
+ /***/ },
3942
+
3943
+ /***/ "./src/tabContextMenu.ts"
3944
+ /*!*******************************!*\
3945
+ !*** ./src/tabContextMenu.ts ***!
3946
+ \*******************************/
3947
+ (__unused_webpack_module, exports, __webpack_require__) {
3948
+
3949
+ "use strict";
3950
+
3951
+ var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
3952
+ var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
3953
+ if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
3954
+ else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
3955
+ return c > 3 && r && Object.defineProperty(target, key, r), r;
3956
+ };
3957
+ var __metadata = (this && this.__metadata) || function (k, v) {
3958
+ if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
3959
+ };
3960
+ Object.defineProperty(exports, "__esModule", ({ value: true }));
3961
+ exports.TmuxContextMenuProvider = void 0;
3962
+ const core_1 = __webpack_require__(/*! @angular/core */ "@angular/core");
3963
+ const tabby_core_1 = __webpack_require__(/*! tabby-core */ "tabby-core");
3964
+ const tabby_terminal_1 = __webpack_require__(/*! tabby-terminal */ "tabby-terminal");
3965
+ const tmux_service_1 = __webpack_require__(/*! ./services/tmux.service */ "./src/services/tmux.service.ts");
3966
+ const tmuxSessionTab_component_1 = __webpack_require__(/*! ./components/tmuxSessionTab.component */ "./src/components/tmuxSessionTab.component.ts");
3967
+ const tmuxPaneTab_component_1 = __webpack_require__(/*! ./components/tmuxPaneTab.component */ "./src/components/tmuxPaneTab.component.ts");
3968
+ /**
3969
+ * TmuxContextMenuProvider - Adds tmux-related items to tab context menu.
3970
+ *
3971
+ * - On a terminal tab: "Enter Tmux Mode"
3972
+ * - On a TmuxSessionTab / TmuxPaneTab: "Exit Tmux Mode" + Split + Close pane
3973
+ */
3974
+ let TmuxContextMenuProvider = class TmuxContextMenuProvider extends tabby_core_1.TabContextMenuItemProvider {
3975
+ constructor(tmuxService) {
3976
+ super();
3977
+ this.tmuxService = tmuxService;
3978
+ this.weight = 5;
3979
+ }
3980
+ async getItems(tab, _tabHeader) {
3981
+ // On a TmuxSessionTab: show exit option
3982
+ if (tab instanceof tmuxSessionTab_component_1.TmuxSessionTabComponent) {
3983
+ return [
3984
+ {
3985
+ label: 'Exit Tmux Mode',
3986
+ click: async () => {
3987
+ await this.tmuxService.disconnect();
3988
+ },
3989
+ },
3990
+ ];
3991
+ }
3992
+ // On a TmuxPaneTab: show exit, split, and close pane
3993
+ if (tab instanceof tmuxPaneTab_component_1.TmuxPaneTabComponent) {
3994
+ const items = [
3995
+ {
3996
+ label: 'Exit Tmux Mode',
3997
+ click: async () => {
3998
+ await this.tmuxService.disconnect();
3999
+ },
4000
+ },
4001
+ {
4002
+ label: 'Split',
4003
+ submenu: [
4004
+ { label: 'Right', click: () => this.splitPane(tab, 'right') },
4005
+ { label: 'Down', click: () => this.splitPane(tab, 'down') },
4006
+ { label: 'Left', click: () => this.splitPane(tab, 'left') },
4007
+ { label: 'Up', click: () => this.splitPane(tab, 'up') },
4008
+ ],
4009
+ },
4010
+ {
4011
+ label: 'Close',
4012
+ click: () => this.closePane(tab),
4013
+ },
4014
+ ];
4015
+ return items;
4016
+ }
4017
+ // On a terminal tab: show enter tmux mode option
4018
+ if (tab instanceof tabby_terminal_1.BaseTerminalTabComponent) {
4019
+ return [
4020
+ {
4021
+ label: 'Enter Tmux Mode',
4022
+ click: async () => {
4023
+ await this.tmuxService.attachToTerminal(tab);
4024
+ },
4025
+ },
4026
+ ];
4027
+ }
4028
+ return [];
4029
+ }
4030
+ async splitPane(paneTab, direction) {
4031
+ const controller = paneTab.controller;
4032
+ if (!controller)
4033
+ return;
4034
+ const paneId = paneTab.paneId;
4035
+ const flagMap = {
4036
+ 'right': '-h',
4037
+ 'down': '-v',
4038
+ 'left': '-h -b',
4039
+ 'up': '-v -b',
4040
+ };
4041
+ const flag = flagMap[direction];
4042
+ await controller.gateway.sendCommand(`split-window ${flag} -t %${paneId}`);
4043
+ // Discover the new pane and trigger layout update
4044
+ await controller.refreshPanes();
4045
+ }
4046
+ async closePane(paneTab) {
4047
+ const controller = paneTab.controller;
4048
+ if (!controller)
4049
+ return;
4050
+ await controller.killPane(paneTab.paneId);
4051
+ }
4052
+ };
4053
+ TmuxContextMenuProvider.ctorParameters = () => [
4054
+ { type: tmux_service_1.TmuxService }
4055
+ ];
4056
+ TmuxContextMenuProvider = __decorate([
4057
+ (0, core_1.Injectable)(),
4058
+ __metadata("design:paramtypes", [tmux_service_1.TmuxService])
4059
+ ], TmuxContextMenuProvider);
4060
+ exports.TmuxContextMenuProvider = TmuxContextMenuProvider;
4061
+
4062
+
4063
+ /***/ },
4064
+
4065
+ /***/ "@angular/common"
4066
+ /*!**********************************!*\
4067
+ !*** external "@angular/common" ***!
4068
+ \**********************************/
4069
+ (module) {
4070
+
4071
+ "use strict";
4072
+ module.exports = __WEBPACK_EXTERNAL_MODULE__angular_common__;
4073
+
4074
+ /***/ },
4075
+
4076
+ /***/ "@angular/core"
4077
+ /*!********************************!*\
4078
+ !*** external "@angular/core" ***!
4079
+ \********************************/
4080
+ (module) {
4081
+
4082
+ "use strict";
4083
+ module.exports = __WEBPACK_EXTERNAL_MODULE__angular_core__;
4084
+
4085
+ /***/ },
4086
+
4087
+ /***/ "@angular/forms"
4088
+ /*!*********************************!*\
4089
+ !*** external "@angular/forms" ***!
4090
+ \*********************************/
4091
+ (module) {
4092
+
4093
+ "use strict";
4094
+ module.exports = __WEBPACK_EXTERNAL_MODULE__angular_forms__;
4095
+
4096
+ /***/ },
4097
+
4098
+ /***/ "rxjs"
4099
+ /*!***********************!*\
4100
+ !*** external "rxjs" ***!
4101
+ \***********************/
4102
+ (module) {
4103
+
4104
+ "use strict";
4105
+ module.exports = __WEBPACK_EXTERNAL_MODULE_rxjs__;
4106
+
4107
+ /***/ },
4108
+
4109
+ /***/ "tabby-core"
4110
+ /*!*****************************!*\
4111
+ !*** external "tabby-core" ***!
4112
+ \*****************************/
4113
+ (module) {
4114
+
4115
+ "use strict";
4116
+ module.exports = __WEBPACK_EXTERNAL_MODULE_tabby_core__;
4117
+
4118
+ /***/ },
4119
+
4120
+ /***/ "tabby-settings"
4121
+ /*!*********************************!*\
4122
+ !*** external "tabby-settings" ***!
4123
+ \*********************************/
4124
+ (module) {
4125
+
4126
+ "use strict";
4127
+ module.exports = __WEBPACK_EXTERNAL_MODULE_tabby_settings__;
4128
+
4129
+ /***/ },
4130
+
4131
+ /***/ "tabby-terminal"
4132
+ /*!*********************************!*\
4133
+ !*** external "tabby-terminal" ***!
4134
+ \*********************************/
4135
+ (module) {
4136
+
4137
+ "use strict";
4138
+ module.exports = __WEBPACK_EXTERNAL_MODULE_tabby_terminal__;
4139
+
4140
+ /***/ }
4141
+
4142
+ /******/ });
4143
+ /************************************************************************/
4144
+ /******/ // The module cache
4145
+ /******/ var __webpack_module_cache__ = {};
4146
+ /******/
4147
+ /******/ // The require function
4148
+ /******/ function __webpack_require__(moduleId) {
4149
+ /******/ // Check if module is in cache
4150
+ /******/ var cachedModule = __webpack_module_cache__[moduleId];
4151
+ /******/ if (cachedModule !== undefined) {
4152
+ /******/ return cachedModule.exports;
4153
+ /******/ }
4154
+ /******/ // Check if module exists (development only)
4155
+ /******/ if (__webpack_modules__[moduleId] === undefined) {
4156
+ /******/ var e = new Error("Cannot find module '" + moduleId + "'");
4157
+ /******/ e.code = 'MODULE_NOT_FOUND';
4158
+ /******/ throw e;
4159
+ /******/ }
4160
+ /******/ // Create a new module (and put it into the cache)
4161
+ /******/ var module = __webpack_module_cache__[moduleId] = {
4162
+ /******/ id: moduleId,
4163
+ /******/ // no module.loaded needed
4164
+ /******/ exports: {}
4165
+ /******/ };
4166
+ /******/
4167
+ /******/ // Execute the module function
4168
+ /******/ __webpack_modules__[moduleId].call(module.exports, module, module.exports, __webpack_require__);
4169
+ /******/
4170
+ /******/ // Return the exports of the module
4171
+ /******/ return module.exports;
4172
+ /******/ }
4173
+ /******/
4174
+ /************************************************************************/
4175
+ /******/ /* webpack/runtime/compat get default export */
4176
+ /******/ (() => {
4177
+ /******/ // getDefaultExport function for compatibility with non-harmony modules
4178
+ /******/ __webpack_require__.n = (module) => {
4179
+ /******/ var getter = module && module.__esModule ?
4180
+ /******/ () => (module['default']) :
4181
+ /******/ () => (module);
4182
+ /******/ __webpack_require__.d(getter, { a: getter });
4183
+ /******/ return getter;
4184
+ /******/ };
4185
+ /******/ })();
4186
+ /******/
4187
+ /******/ /* webpack/runtime/define property getters */
4188
+ /******/ (() => {
4189
+ /******/ // define getter functions for harmony exports
4190
+ /******/ __webpack_require__.d = (exports, definition) => {
4191
+ /******/ for(var key in definition) {
4192
+ /******/ if(__webpack_require__.o(definition, key) && !__webpack_require__.o(exports, key)) {
4193
+ /******/ Object.defineProperty(exports, key, { enumerable: true, get: definition[key] });
4194
+ /******/ }
4195
+ /******/ }
4196
+ /******/ };
4197
+ /******/ })();
4198
+ /******/
4199
+ /******/ /* webpack/runtime/hasOwnProperty shorthand */
4200
+ /******/ (() => {
4201
+ /******/ __webpack_require__.o = (obj, prop) => (Object.prototype.hasOwnProperty.call(obj, prop))
4202
+ /******/ })();
4203
+ /******/
4204
+ /******/ /* webpack/runtime/make namespace object */
4205
+ /******/ (() => {
4206
+ /******/ // define __esModule on exports
4207
+ /******/ __webpack_require__.r = (exports) => {
4208
+ /******/ if(typeof Symbol !== 'undefined' && Symbol.toStringTag) {
4209
+ /******/ Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
4210
+ /******/ }
4211
+ /******/ Object.defineProperty(exports, '__esModule', { value: true });
4212
+ /******/ };
4213
+ /******/ })();
4214
+ /******/
4215
+ /************************************************************************/
4216
+ /******/
4217
+ /******/ // startup
4218
+ /******/ // Load entry module and return exports
4219
+ /******/ // This entry module is referenced by other modules so it can't be inlined
4220
+ /******/ var __webpack_exports__ = __webpack_require__("./src/index.ts");
4221
+ /******/
4222
+ /******/ return __webpack_exports__;
4223
+ /******/ })()
4224
+ ;
4225
+ });
4226
+ //# sourceMappingURL=index.js.map