tabby-agentdeck 0.1.0 → 0.2.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/README.md +178 -240
- package/dist/api.d.ts +10 -0
- package/dist/claudeHooks.d.ts +22 -0
- package/dist/config.d.ts +156 -34
- package/dist/deck.service.d.ts +140 -29
- package/dist/dock.d.ts +62 -0
- package/dist/index.js +2319 -316
- package/dist/index.js.map +1 -1
- package/dist/notify.service.d.ts +19 -6
- package/dist/screen.d.ts +42 -0
- package/dist/settings.component.d.ts +52 -0
- package/dist/settings.d.ts +11 -0
- package/dist/status.service.d.ts +9 -0
- package/hooks/agentdeck-notify.ps1 +22 -23
- package/install.ps1 +60 -2
- package/package.json +50 -50
package/dist/index.js
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
(function webpackUniversalModuleDefinition(root, factory) {
|
|
2
2
|
if(typeof exports === 'object' && typeof module === 'object')
|
|
3
|
-
module.exports = factory(require("@angular/common"), require("@angular/core"), (function webpackLoadOptionalExternalModule() { try { return require("electron"); } catch(e) {} }()), require("fs"), require("os"), require("path"), require("rxjs"), require("tabby-core"), require("tabby-settings"), require("tabby-terminal"));
|
|
3
|
+
module.exports = factory(require("@angular/common"), require("@angular/core"), require("@angular/forms"), (function webpackLoadOptionalExternalModule() { try { return require("electron"); } catch(e) {} }()), require("fs"), require("os"), require("path"), require("rxjs"), require("tabby-core"), require("tabby-settings"), require("tabby-terminal"));
|
|
4
4
|
else if(typeof define === 'function' && define.amd)
|
|
5
|
-
define(["@angular/common", "@angular/core", "electron", "fs", "os", "path", "rxjs", "tabby-core", "tabby-settings", "tabby-terminal"], factory);
|
|
5
|
+
define(["@angular/common", "@angular/core", "@angular/forms", "electron", "fs", "os", "path", "rxjs", "tabby-core", "tabby-settings", "tabby-terminal"], factory);
|
|
6
6
|
else {
|
|
7
|
-
var a = typeof exports === 'object' ? factory(require("@angular/common"), require("@angular/core"), (function webpackLoadOptionalExternalModule() { try { return require("electron"); } catch(e) {} }()), require("fs"), require("os"), require("path"), require("rxjs"), require("tabby-core"), require("tabby-settings"), require("tabby-terminal")) : factory(root["@angular/common"], root["@angular/core"], root["electron"], root["fs"], root["os"], root["path"], root["rxjs"], root["tabby-core"], root["tabby-settings"], root["tabby-terminal"]);
|
|
7
|
+
var a = typeof exports === 'object' ? factory(require("@angular/common"), require("@angular/core"), require("@angular/forms"), (function webpackLoadOptionalExternalModule() { try { return require("electron"); } catch(e) {} }()), require("fs"), require("os"), require("path"), require("rxjs"), require("tabby-core"), require("tabby-settings"), require("tabby-terminal")) : factory(root["@angular/common"], root["@angular/core"], root["@angular/forms"], root["electron"], root["fs"], root["os"], root["path"], root["rxjs"], root["tabby-core"], root["tabby-settings"], root["tabby-terminal"]);
|
|
8
8
|
for(var i in a) (typeof exports === 'object' ? exports : root)[i] = a[i];
|
|
9
9
|
}
|
|
10
|
-
})(global, (__WEBPACK_EXTERNAL_MODULE__angular_common__, __WEBPACK_EXTERNAL_MODULE__angular_core__, __WEBPACK_EXTERNAL_MODULE_electron__, __WEBPACK_EXTERNAL_MODULE_fs__, __WEBPACK_EXTERNAL_MODULE_os__, __WEBPACK_EXTERNAL_MODULE_path__, __WEBPACK_EXTERNAL_MODULE_rxjs__, __WEBPACK_EXTERNAL_MODULE_tabby_core__, __WEBPACK_EXTERNAL_MODULE_tabby_settings__, __WEBPACK_EXTERNAL_MODULE_tabby_terminal__) => {
|
|
10
|
+
})(global, (__WEBPACK_EXTERNAL_MODULE__angular_common__, __WEBPACK_EXTERNAL_MODULE__angular_core__, __WEBPACK_EXTERNAL_MODULE__angular_forms__, __WEBPACK_EXTERNAL_MODULE_electron__, __WEBPACK_EXTERNAL_MODULE_fs__, __WEBPACK_EXTERNAL_MODULE_os__, __WEBPACK_EXTERNAL_MODULE_path__, __WEBPACK_EXTERNAL_MODULE_rxjs__, __WEBPACK_EXTERNAL_MODULE_tabby_core__, __WEBPACK_EXTERNAL_MODULE_tabby_settings__, __WEBPACK_EXTERNAL_MODULE_tabby_terminal__) => {
|
|
11
11
|
return /******/ (() => { // webpackBootstrap
|
|
12
12
|
/******/ "use strict";
|
|
13
13
|
/******/ var __webpack_modules__ = ({
|
|
@@ -31,19 +31,47 @@ __webpack_require__.r(__webpack_exports__);
|
|
|
31
31
|
|
|
32
32
|
var ___CSS_LOADER_EXPORT___ = _node_modules_css_loader_dist_runtime_api_js__WEBPACK_IMPORTED_MODULE_1___default()((_node_modules_css_loader_dist_runtime_sourceMaps_js__WEBPACK_IMPORTED_MODULE_0___default()));
|
|
33
33
|
// Module
|
|
34
|
-
___CSS_LOADER_EXPORT___.push([module.id,
|
|
34
|
+
___CSS_LOADER_EXPORT___.push([module.id, `body.agentdeck-active.ad-bg-clip .content-tab-active::before {
|
|
35
|
+
left: var(--ad-bg-left) !important;
|
|
36
|
+
top: var(--ad-bg-top) !important;
|
|
37
|
+
right: auto !important;
|
|
38
|
+
bottom: auto !important;
|
|
39
|
+
width: var(--ad-bg-width) !important;
|
|
40
|
+
height: var(--ad-bg-height) !important;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
#agentdeck-sidebar {
|
|
35
44
|
display: flex;
|
|
36
45
|
flex-direction: column;
|
|
37
|
-
|
|
38
|
-
height: 100%;
|
|
39
|
-
min-width: 180px;
|
|
46
|
+
z-index: 5;
|
|
40
47
|
overflow: hidden;
|
|
41
48
|
background: var(--ad-panel-bg, var(--bs-body-bg, #1e2127));
|
|
42
|
-
border-left: 1px solid rgba(255, 255, 255, 0.08);
|
|
43
49
|
font-size: 12px;
|
|
44
50
|
user-select: none;
|
|
45
51
|
-webkit-app-region: no-drag;
|
|
46
52
|
}
|
|
53
|
+
#agentdeck-sidebar.ad-dock-right {
|
|
54
|
+
border-left: 1px solid rgba(255, 255, 255, 0.08);
|
|
55
|
+
}
|
|
56
|
+
#agentdeck-sidebar.ad-dock-left {
|
|
57
|
+
border-right: 1px solid rgba(255, 255, 255, 0.08);
|
|
58
|
+
}
|
|
59
|
+
#agentdeck-sidebar.ad-dock-top {
|
|
60
|
+
border-bottom: 1px solid rgba(255, 255, 255, 0.08);
|
|
61
|
+
}
|
|
62
|
+
#agentdeck-sidebar.ad-dock-bottom {
|
|
63
|
+
border-top: 1px solid rgba(255, 255, 255, 0.08);
|
|
64
|
+
}
|
|
65
|
+
#agentdeck-sidebar.ad-dock-top .ad-list, #agentdeck-sidebar.ad-dock-bottom .ad-list {
|
|
66
|
+
display: flex;
|
|
67
|
+
flex-wrap: wrap;
|
|
68
|
+
align-content: flex-start;
|
|
69
|
+
gap: 0 6px;
|
|
70
|
+
overflow-y: auto;
|
|
71
|
+
}
|
|
72
|
+
#agentdeck-sidebar.ad-dock-top .ad-tab, #agentdeck-sidebar.ad-dock-bottom .ad-tab {
|
|
73
|
+
flex: 0 0 220px;
|
|
74
|
+
}
|
|
47
75
|
#agentdeck-sidebar .ad-head {
|
|
48
76
|
display: flex;
|
|
49
77
|
align-items: center;
|
|
@@ -53,7 +81,46 @@ ___CSS_LOADER_EXPORT___.push([module.id, `#agentdeck-sidebar {
|
|
|
53
81
|
font-size: 10px;
|
|
54
82
|
font-weight: 700;
|
|
55
83
|
color: rgba(255, 255, 255, 0.4);
|
|
56
|
-
-webkit-app-region: drag;
|
|
84
|
+
-webkit-app-region: no-drag;
|
|
85
|
+
cursor: grab;
|
|
86
|
+
}
|
|
87
|
+
#agentdeck-sidebar .ad-head:active {
|
|
88
|
+
cursor: grabbing;
|
|
89
|
+
}
|
|
90
|
+
#agentdeck-sidebar .ad-resize {
|
|
91
|
+
position: absolute;
|
|
92
|
+
z-index: 6;
|
|
93
|
+
background: transparent;
|
|
94
|
+
transition: background 0.12s ease;
|
|
95
|
+
}
|
|
96
|
+
#agentdeck-sidebar .ad-resize:hover, body.ad-resizing #agentdeck-sidebar .ad-resize {
|
|
97
|
+
background: rgba(97, 175, 239, 0.5);
|
|
98
|
+
}
|
|
99
|
+
#agentdeck-sidebar .ad-resize-right,
|
|
100
|
+
#agentdeck-sidebar .ad-resize-left {
|
|
101
|
+
top: 0;
|
|
102
|
+
bottom: 0;
|
|
103
|
+
width: 6px;
|
|
104
|
+
cursor: col-resize;
|
|
105
|
+
}
|
|
106
|
+
#agentdeck-sidebar .ad-resize-right {
|
|
107
|
+
left: 0;
|
|
108
|
+
}
|
|
109
|
+
#agentdeck-sidebar .ad-resize-left {
|
|
110
|
+
right: 0;
|
|
111
|
+
}
|
|
112
|
+
#agentdeck-sidebar .ad-resize-top,
|
|
113
|
+
#agentdeck-sidebar .ad-resize-bottom {
|
|
114
|
+
left: 0;
|
|
115
|
+
right: 0;
|
|
116
|
+
height: 6px;
|
|
117
|
+
cursor: row-resize;
|
|
118
|
+
}
|
|
119
|
+
#agentdeck-sidebar .ad-resize-bottom {
|
|
120
|
+
top: 0;
|
|
121
|
+
}
|
|
122
|
+
#agentdeck-sidebar .ad-resize-top {
|
|
123
|
+
bottom: 0;
|
|
57
124
|
}
|
|
58
125
|
#agentdeck-sidebar .ad-head-count {
|
|
59
126
|
-webkit-app-region: no-drag;
|
|
@@ -136,12 +203,24 @@ ___CSS_LOADER_EXPORT___.push([module.id, `#agentdeck-sidebar {
|
|
|
136
203
|
text-decoration: underline dotted;
|
|
137
204
|
text-underline-offset: 2px;
|
|
138
205
|
}
|
|
206
|
+
#agentdeck-sidebar .ad-prompt {
|
|
207
|
+
display: flex;
|
|
208
|
+
align-items: center;
|
|
209
|
+
margin-top: 2px;
|
|
210
|
+
min-width: 0;
|
|
211
|
+
font-size: 11px;
|
|
212
|
+
line-height: 1.4;
|
|
213
|
+
}
|
|
139
214
|
#agentdeck-sidebar .ad-label {
|
|
215
|
+
flex: 1 1 auto;
|
|
140
216
|
overflow: hidden;
|
|
141
217
|
text-overflow: ellipsis;
|
|
142
218
|
white-space: nowrap;
|
|
143
219
|
color: rgba(255, 255, 255, 0.6);
|
|
144
220
|
}
|
|
221
|
+
#agentdeck-sidebar .ad-label.empty {
|
|
222
|
+
color: rgba(255, 255, 255, 0.25);
|
|
223
|
+
}
|
|
145
224
|
#agentdeck-sidebar .ad-elapsed {
|
|
146
225
|
flex: 0 0 auto;
|
|
147
226
|
margin-left: auto;
|
|
@@ -202,6 +281,12 @@ ___CSS_LOADER_EXPORT___.push([module.id, `#agentdeck-sidebar {
|
|
|
202
281
|
background: rgba(255, 255, 255, 0.08);
|
|
203
282
|
color: #fff;
|
|
204
283
|
}
|
|
284
|
+
#agentdeck-sidebar .ad-repair {
|
|
285
|
+
flex: 0 0 auto;
|
|
286
|
+
min-width: 28px;
|
|
287
|
+
font-size: 13px;
|
|
288
|
+
line-height: 1;
|
|
289
|
+
}
|
|
205
290
|
|
|
206
291
|
.ad-menu {
|
|
207
292
|
position: fixed;
|
|
@@ -226,14 +311,74 @@ ___CSS_LOADER_EXPORT___.push([module.id, `#agentdeck-sidebar {
|
|
|
226
311
|
color: #fff;
|
|
227
312
|
}
|
|
228
313
|
|
|
314
|
+
.ad-dock-overlay {
|
|
315
|
+
position: fixed;
|
|
316
|
+
inset: 0;
|
|
317
|
+
z-index: 9998;
|
|
318
|
+
pointer-events: none;
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
.ad-dock-preview {
|
|
322
|
+
position: fixed;
|
|
323
|
+
display: none;
|
|
324
|
+
border: 1px solid rgba(97, 175, 239, 0.9);
|
|
325
|
+
background: rgba(97, 175, 239, 0.22);
|
|
326
|
+
box-shadow: 0 0 0 9999px rgba(0, 0, 0, 0.18);
|
|
327
|
+
transition: left 0.08s ease, top 0.08s ease, width 0.08s ease, height 0.08s ease;
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
.ad-dock-zone {
|
|
331
|
+
position: fixed;
|
|
332
|
+
width: 46px;
|
|
333
|
+
height: 46px;
|
|
334
|
+
margin: -23px 0 0 -23px;
|
|
335
|
+
border: 1px solid rgba(255, 255, 255, 0.35);
|
|
336
|
+
border-radius: 6px;
|
|
337
|
+
background: rgba(38, 42, 49, 0.92);
|
|
338
|
+
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.45);
|
|
339
|
+
display: flex;
|
|
340
|
+
align-items: center;
|
|
341
|
+
justify-content: center;
|
|
342
|
+
}
|
|
343
|
+
.ad-dock-zone .ad-dock-mark {
|
|
344
|
+
width: 26px;
|
|
345
|
+
height: 26px;
|
|
346
|
+
border: 1px solid rgba(255, 255, 255, 0.45);
|
|
347
|
+
border-radius: 3px;
|
|
348
|
+
}
|
|
349
|
+
.ad-dock-zone.active {
|
|
350
|
+
border-color: rgba(97, 175, 239, 0.95);
|
|
351
|
+
background: rgba(97, 175, 239, 0.35);
|
|
352
|
+
}
|
|
353
|
+
.ad-dock-zone.ad-dock-zone-left .ad-dock-mark {
|
|
354
|
+
border-left: 4px solid rgba(97, 175, 239, 0.95);
|
|
355
|
+
}
|
|
356
|
+
.ad-dock-zone.ad-dock-zone-right .ad-dock-mark {
|
|
357
|
+
border-right: 4px solid rgba(97, 175, 239, 0.95);
|
|
358
|
+
}
|
|
359
|
+
.ad-dock-zone.ad-dock-zone-top .ad-dock-mark {
|
|
360
|
+
border-top: 4px solid rgba(97, 175, 239, 0.95);
|
|
361
|
+
}
|
|
362
|
+
.ad-dock-zone.ad-dock-zone-bottom .ad-dock-mark {
|
|
363
|
+
border-bottom: 4px solid rgba(97, 175, 239, 0.95);
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
body.ad-resizing {
|
|
367
|
+
cursor: inherit;
|
|
368
|
+
}
|
|
369
|
+
body.ad-resizing .content.main {
|
|
370
|
+
pointer-events: none;
|
|
371
|
+
}
|
|
372
|
+
|
|
229
373
|
body.agentdeck-active .content.main > .tab-bar {
|
|
230
374
|
display: none !important;
|
|
231
375
|
}
|
|
232
376
|
body.agentdeck-active .window {
|
|
233
377
|
display: flex;
|
|
234
378
|
flex-direction: row;
|
|
379
|
+
position: relative;
|
|
235
380
|
background: var(--ad-panel-bg, var(--bs-body-bg, #1e2127));
|
|
236
|
-
}`, "",{"version":3,"sources":["webpack://./src/styles.scss"],"names":[],"mappings":"AAGA;EACI,aAAA;EACA,sBAAA;EACA,cAAA;EACA,YAAA;EACA,gBAAA;EACA,gBAAA;EAEA,0DAAA;EACA,gDAAA;EACA,eAAA;EACA,iBAAA;EACA,2BAAA;AAHJ;AAKI;EACI,aAAA;EACA,mBAAA;EACA,8BAAA;EACA,sBAAA;EACA,sBAAA;EACA,eAAA;EACA,gBAAA;EACA,+BAAA;EAEA,wBAAA;AAJR;AAOI;EACI,2BAAA;EACA,gBAAA;EACA,kBAAA;EACA,qCAAA;EACA,gCAAA;AALR;AASI;EACI,cAAA;EACA,gBAAA;EACA,kBAAA;EACA,oBAAA;AAPR;AASQ;EACI,UAAA;AAPZ;AAUQ;EACI,qCAAA;EACA,kBAAA;AARZ;AAUY;EACI,qCAAA;AARhB;AAaI;EACI,kBAAA;EACA,aAAA;EACA,oBAAA;EACA,QAAA;EACA,kBAAA;EACA,sBAAA;EACA,kBAAA;EACA,eAAA;EACA,gCAAA;AAXR;AAaQ;EACI,qCAAA;AAXZ;AAaY;EACI,aAAA;AAXhB;AAeQ;EACI,oCAAA;AAbZ;AAeY;EACI,WAAA;AAbhB;AAmBI;EACI,aAAA;EACA,kBAAA;EACA,oCAAA;AAjBR;AAoBI;EACI,cAAA;EACA,YAAA;AAlBR;AAqBI;EACI,gBAAA;EACA,uBAAA;EACA,mBAAA;EACA,gCAAA;EACA,iBAAA;EACA,iBAAA;AAnBR;AAsBI;EACI,aAAA;EACA,mBAAA;EACA,QAAA;EACA,eAAA;EACA,iBAAA;EACA,gBAAA;EACA,gCAAA;AApBR;AAuBI;EACI,cAAA;EACA,+BAAA;EACA,gBAAA;AArBR;AAwBQ;EACI,iCAAA;EACA,0BAAA;AAtBZ;AA0BI;EACI,gBAAA;EACA,uBAAA;EACA,mBAAA;EACA,+BAAA;AAxBR;AA2BI;EACI,cAAA;EACA,iBAAA;EACA,kCAAA;EACA,aAAA;AAzBR;AA4BI;EACI,cAAA;EACA,YAAA;EACA,cAAA;EACA,2CAAA;EACA,kBAAA;EACA,+BAAA;EACA,WAAA;EACA,iBAAA;EACA,aAAA;AA1BR;AA4BQ;EACI,sCAAA;AA1BZ;AA8BI;EACI,cAAA;EACA,kBAAA;EACA,WAAA;EACA,YAAA;EACA,UAAA;EACA,SAAA;EACA,kBAAA;EACA,UAAA;EACA,uBAAA;EACA,+BAAA;EACA,eAAA;EACA,cAAA;EACA,eAAA;EACA,mDAAA;AA5BR;AA8BQ;EACI,qBAAA;EACA,mCAAA;AA5BZ;AAgCI;EACI,aAAA;EACA,QAAA;EACA,YAAA;EACA,+CAAA;AA9BR;AAiCI;EACI,cAAA;EACA,gBAAA;EACA,2CAAA;EACA,kBAAA;EACA,uBAAA;EACA,+BAAA;EACA,eAAA;EACA,eAAA;AA/BR;AAiCQ;EACI,qCAAA;EACA,WAAA;AA/BZ;;AAqCA;EACI,eAAA;EACA,cAAA;EACA,gBAAA;EACA,YAAA;EACA,2CAAA;EACA,kBAAA;EACA,mBAAA;EACA,yCAAA;EACA,eAAA;EACA,iBAAA;AAlCJ;AAoCI;EACI,iBAAA;EACA,kBAAA;EACA,gCAAA;EACA,eAAA;AAlCR;AAoCQ;EACI,oCAAA;EACA,WAAA;AAlCZ;;AAyCI;EACI,wBAAA;AAtCR;AAyCI;EACI,aAAA;EACA,mBAAA;EAGA,0DAAA;AAzCR","sourcesContent":["// AgentDeck 사이드바 스타일\n// Tabby 테마 변수(--bs-*)를 우선 쓰되, 없을 때를 대비해 폴백 색을 함께 준다.\n\n#agentdeck-sidebar {\n display: flex;\n flex-direction: column;\n flex: 0 0 auto;\n height: 100%;\n min-width: 180px;\n overflow: hidden;\n // --ad-panel-bg 는 agentdeck 이 sidebarOpacity 로 내려준다 (배경 이미지가 비치게)\n background: var(--ad-panel-bg, var(--bs-body-bg, #1e2127));\n border-left: 1px solid rgba(255, 255, 255, 0.08);\n font-size: 12px;\n user-select: none;\n -webkit-app-region: no-drag;\n\n .ad-head {\n display: flex;\n align-items: center;\n justify-content: space-between;\n padding: 10px 12px 8px;\n letter-spacing: 0.12em;\n font-size: 10px;\n font-weight: 700;\n color: rgba(255, 255, 255, 0.4);\n // 창 상단을 잡아 끌 수 있게 (프레임리스 대비)\n -webkit-app-region: drag;\n }\n\n .ad-head-count {\n -webkit-app-region: no-drag;\n padding: 1px 6px;\n border-radius: 8px;\n background: rgba(255, 255, 255, 0.08);\n color: rgba(255, 255, 255, 0.55);\n }\n\n // 요구 2-1: 탭이 많아지면 이 영역만 스크롤된다\n .ad-list {\n flex: 1 1 auto;\n overflow-y: auto;\n overflow-x: hidden;\n padding: 2px 6px 8px;\n\n &::-webkit-scrollbar {\n width: 8px;\n }\n\n &::-webkit-scrollbar-thumb {\n background: rgba(255, 255, 255, 0.14);\n border-radius: 4px;\n\n &:hover {\n background: rgba(255, 255, 255, 0.24);\n }\n }\n }\n\n .ad-tab {\n position: relative;\n display: flex;\n align-items: stretch;\n gap: 8px;\n margin-bottom: 3px;\n padding: 7px 6px 7px 0;\n border-radius: 5px;\n cursor: pointer;\n transition: background 0.1s ease;\n\n &:hover {\n background: rgba(255, 255, 255, 0.05);\n\n .ad-close {\n opacity: 0.65;\n }\n }\n\n &.active {\n background: rgba(255, 255, 255, 0.1);\n\n .ad-title {\n color: #fff;\n }\n }\n }\n\n // 상태색 표시 바\n .ad-bar {\n flex: 0 0 3px;\n border-radius: 2px;\n background: var(--ad-color, #6b727d);\n }\n\n .ad-body {\n flex: 1 1 auto;\n min-width: 0;\n }\n\n .ad-title {\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n color: rgba(255, 255, 255, 0.82);\n font-size: 12.5px;\n line-height: 1.35;\n }\n\n .ad-meta {\n display: flex;\n align-items: center;\n gap: 6px;\n margin-top: 2px;\n font-size: 10.5px;\n line-height: 1.4;\n color: rgba(255, 255, 255, 0.45);\n }\n\n .ad-badge {\n flex: 0 0 auto;\n color: var(--ad-color, #6b727d);\n font-weight: 600;\n\n // 수동/훅으로 고정된 상태는 밑줄로 구분\n &.pinned {\n text-decoration: underline dotted;\n text-underline-offset: 2px;\n }\n }\n\n .ad-label {\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n color: rgba(255, 255, 255, 0.6);\n }\n\n .ad-elapsed {\n flex: 0 0 auto;\n margin-left: auto;\n font-variant-numeric: tabular-nums;\n opacity: 0.55;\n }\n\n .ad-label-input {\n flex: 1 1 auto;\n min-width: 0;\n padding: 0 4px;\n border: 1px solid rgba(255, 255, 255, 0.25);\n border-radius: 3px;\n background: rgba(0, 0, 0, 0.35);\n color: #fff;\n font-size: 10.5px;\n outline: none;\n\n &:focus {\n border-color: var(--ad-color, #61afef);\n }\n }\n\n .ad-close {\n flex: 0 0 auto;\n align-self: center;\n width: 18px;\n height: 18px;\n padding: 0;\n border: 0;\n border-radius: 3px;\n opacity: 0;\n background: transparent;\n color: rgba(255, 255, 255, 0.8);\n font-size: 15px;\n line-height: 1;\n cursor: pointer;\n transition: opacity 0.1s ease, background 0.1s ease;\n\n &:hover {\n opacity: 1 !important;\n background: rgba(255, 80, 80, 0.35);\n }\n }\n\n .ad-foot {\n display: flex;\n gap: 6px;\n padding: 8px;\n border-top: 1px solid rgba(255, 255, 255, 0.07);\n }\n\n .ad-btn {\n flex: 1 1 auto;\n padding: 5px 8px;\n border: 1px solid rgba(255, 255, 255, 0.12);\n border-radius: 4px;\n background: transparent;\n color: rgba(255, 255, 255, 0.6);\n font-size: 11px;\n cursor: pointer;\n\n &:hover {\n background: rgba(255, 255, 255, 0.08);\n color: #fff;\n }\n }\n}\n\n// 우클릭 상태 메뉴\n.ad-menu {\n position: fixed;\n z-index: 10000;\n min-width: 130px;\n padding: 4px;\n border: 1px solid rgba(255, 255, 255, 0.12);\n border-radius: 6px;\n background: #262a31;\n box-shadow: 0 6px 24px rgba(0, 0, 0, 0.5);\n font-size: 12px;\n user-select: none;\n\n .ad-menu-item {\n padding: 6px 10px;\n border-radius: 4px;\n color: rgba(255, 255, 255, 0.75);\n cursor: pointer;\n\n &:hover {\n background: rgba(255, 255, 255, 0.1);\n color: #fff;\n }\n }\n}\n\n// AgentDeck 이 켜져 있을 때만 순정 탭바를 감춘다 (사이드바가 그 역할을 대신함)\nbody.agentdeck-active {\n .content.main > .tab-bar {\n display: none !important;\n }\n\n .window {\n display: flex;\n flex-direction: row;\n // 4:3 터미널 왼쪽에 남는 여백이 밝은 기본 배경으로 튀지 않게 한다\n // (--ad-panel-bg 가 있으면 배경 이미지가 이 여백으로도 비친다)\n background: var(--ad-panel-bg, var(--bs-body-bg, #1e2127));\n }\n}\n"],"sourceRoot":""}]);
|
|
381
|
+
}`, "",{"version":3,"sources":["webpack://./src/styles.scss"],"names":[],"mappings":"AAMA;EACI,kCAAA;EACA,gCAAA;EACA,sBAAA;EACA,uBAAA;EACA,oCAAA;EACA,sCAAA;AALJ;;AAQA;EACI,aAAA;EACA,sBAAA;EAEA,UAAA;EACA,gBAAA;EAEA,0DAAA;EACA,eAAA;EACA,iBAAA;EACA,2BAAA;AAPJ;AAUI;EAAkB,gDAAA;AAPtB;AAQI;EAAiB,iDAAA;AALrB;AAMI;EAAgB,kDAAA;AAHpB;AAII;EAAmB,+CAAA;AADvB;AAMQ;EACI,aAAA;EACA,eAAA;EACA,yBAAA;EACA,UAAA;EACA,gBAAA;AAJZ;AAOQ;EACI,eAAA;AALZ;AASI;EACI,aAAA;EACA,mBAAA;EACA,8BAAA;EACA,sBAAA;EACA,sBAAA;EACA,eAAA;EACA,gBAAA;EACA,+BAAA;EAEA,2BAAA;EACA,YAAA;AARR;AAUQ;EACI,gBAAA;AARZ;AAaI;EACI,kBAAA;EACA,UAAA;EACA,uBAAA;EACA,iCAAA;AAXR;AAaQ;EAEI,mCAAA;AAZZ;AAgBI;;EAEI,MAAA;EACA,SAAA;EACA,UAAA;EACA,kBAAA;AAdR;AAiBI;EAAmB,OAAA;AAdvB;AAeI;EAAkB,QAAA;AAZtB;AAcI;;EAEI,OAAA;EACA,QAAA;EACA,WAAA;EACA,kBAAA;AAZR;AAeI;EAAoB,MAAA;AAZxB;AAaI;EAAiB,SAAA;AAVrB;AAYI;EACI,2BAAA;EACA,gBAAA;EACA,kBAAA;EACA,qCAAA;EACA,gCAAA;AAVR;AAcI;EACI,cAAA;EACA,gBAAA;EACA,kBAAA;EACA,oBAAA;AAZR;AAcQ;EACI,UAAA;AAZZ;AAeQ;EACI,qCAAA;EACA,kBAAA;AAbZ;AAeY;EACI,qCAAA;AAbhB;AAkBI;EACI,kBAAA;EACA,aAAA;EACA,oBAAA;EACA,QAAA;EACA,kBAAA;EACA,sBAAA;EACA,kBAAA;EACA,eAAA;EACA,gCAAA;AAhBR;AAkBQ;EACI,qCAAA;AAhBZ;AAkBY;EACI,aAAA;AAhBhB;AAoBQ;EACI,oCAAA;AAlBZ;AAoBY;EACI,WAAA;AAlBhB;AAwBI;EACI,aAAA;EACA,kBAAA;EACA,oCAAA;AAtBR;AAyBI;EACI,cAAA;EACA,YAAA;AAvBR;AA0BI;EACI,gBAAA;EACA,uBAAA;EACA,mBAAA;EACA,gCAAA;EACA,iBAAA;EACA,iBAAA;AAxBR;AA2BI;EACI,aAAA;EACA,mBAAA;EACA,QAAA;EACA,eAAA;EACA,iBAAA;EACA,gBAAA;EACA,gCAAA;AAzBR;AA4BI;EACI,cAAA;EACA,+BAAA;EACA,gBAAA;AA1BR;AA6BQ;EACI,iCAAA;EACA,0BAAA;AA3BZ;AAgCI;EACI,aAAA;EACA,mBAAA;EACA,eAAA;EACA,YAAA;EACA,eAAA;EACA,gBAAA;AA9BR;AAiCI;EACI,cAAA;EACA,gBAAA;EACA,uBAAA;EACA,mBAAA;EACA,+BAAA;AA/BR;AAkCQ;EACI,gCAAA;AAhCZ;AAoCI;EACI,cAAA;EACA,iBAAA;EACA,kCAAA;EACA,aAAA;AAlCR;AAqCI;EACI,cAAA;EACA,YAAA;EACA,cAAA;EACA,2CAAA;EACA,kBAAA;EACA,+BAAA;EACA,WAAA;EACA,iBAAA;EACA,aAAA;AAnCR;AAqCQ;EACI,sCAAA;AAnCZ;AAuCI;EACI,cAAA;EACA,kBAAA;EACA,WAAA;EACA,YAAA;EACA,UAAA;EACA,SAAA;EACA,kBAAA;EACA,UAAA;EACA,uBAAA;EACA,+BAAA;EACA,eAAA;EACA,cAAA;EACA,eAAA;EACA,mDAAA;AArCR;AAuCQ;EACI,qBAAA;EACA,mCAAA;AArCZ;AAyCI;EACI,aAAA;EACA,QAAA;EACA,YAAA;EACA,+CAAA;AAvCR;AA0CI;EACI,cAAA;EACA,gBAAA;EACA,2CAAA;EACA,kBAAA;EACA,uBAAA;EACA,+BAAA;EACA,eAAA;EACA,eAAA;AAxCR;AA0CQ;EACI,qCAAA;EACA,WAAA;AAxCZ;AA6CI;EACI,cAAA;EACA,eAAA;EACA,eAAA;EACA,cAAA;AA3CR;;AAgDA;EACI,eAAA;EACA,cAAA;EACA,gBAAA;EACA,YAAA;EACA,2CAAA;EACA,kBAAA;EACA,mBAAA;EACA,yCAAA;EACA,eAAA;EACA,iBAAA;AA7CJ;AA+CI;EACI,iBAAA;EACA,kBAAA;EACA,gCAAA;EACA,eAAA;AA7CR;AA+CQ;EACI,oCAAA;EACA,WAAA;AA7CZ;;AAmDA;EACI,eAAA;EACA,QAAA;EACA,aAAA;EAEA,oBAAA;AAjDJ;;AAqDA;EACI,eAAA;EACA,aAAA;EACA,yCAAA;EACA,oCAAA;EACA,4CAAA;EACA,gFAAA;AAlDJ;;AAsDA;EACI,eAAA;EACA,WAAA;EACA,YAAA;EACA,uBAAA;EACA,2CAAA;EACA,kBAAA;EACA,kCAAA;EACA,0CAAA;EACA,aAAA;EACA,mBAAA;EACA,uBAAA;AAnDJ;AAqDI;EACI,WAAA;EACA,YAAA;EACA,2CAAA;EACA,kBAAA;AAnDR;AAsDI;EACI,sCAAA;EACA,oCAAA;AApDR;AAwDI;EAAoC,+CAAA;AArDxC;AAsDI;EAAqC,gDAAA;AAnDzC;AAoDI;EAAmC,8CAAA;AAjDvC;AAkDI;EAAsC,iDAAA;AA/C1C;;AAmDA;EACI,eAAA;AAhDJ;AAkDI;EACI,oBAAA;AAhDR;;AAsDI;EACI,wBAAA;AAnDR;AAsDI;EACI,aAAA;EACA,mBAAA;EAEA,kBAAA;EAGA,0DAAA;AAvDR","sourcesContent":["// AgentDeck 사이드바 스타일\r\n// Tabby 테마 변수(--bs-*)를 우선 쓰되, 없을 때를 대비해 폴백 색을 함께 준다.\r\n\r\n// tabby-background 의 배경 이미지 레이어(.content-tab-active::before)를 터미널 영역으로 자른다.\r\n// 좌표 변수(--ad-bg-*)는 deck.service 의 syncBackgroundClip 이 relayout 마다 내려준다.\r\n// 그 플러그인은 런타임에 <style> 을 만들어 head 뒤쪽에 붙이므로 순서로는 못 이긴다 — !important 필수.\r\nbody.agentdeck-active.ad-bg-clip .content-tab-active::before {\r\n left: var(--ad-bg-left) !important;\r\n top: var(--ad-bg-top) !important;\r\n right: auto !important;\r\n bottom: auto !important;\r\n width: var(--ad-bg-width) !important;\r\n height: var(--ad-bg-height) !important;\r\n}\r\n\r\n#agentdeck-sidebar {\r\n display: flex;\r\n flex-direction: column;\r\n // 실제 위치와 크기는 deck.service 의 relayout 이 인라인으로 준다 (도킹 방향에 따라 달라짐)\r\n z-index: 5;\r\n overflow: hidden;\r\n // --ad-panel-bg 는 agentdeck 이 sidebarOpacity 로 내려준다 (배경 이미지가 비치게)\r\n background: var(--ad-panel-bg, var(--bs-body-bg, #1e2127));\r\n font-size: 12px;\r\n user-select: none;\r\n -webkit-app-region: no-drag;\r\n\r\n // 붙어 있는 쪽 반대편에만 경계선을 그린다\r\n &.ad-dock-right { border-left: 1px solid rgba(255, 255, 255, 0.08); }\r\n &.ad-dock-left { border-right: 1px solid rgba(255, 255, 255, 0.08); }\r\n &.ad-dock-top { border-bottom: 1px solid rgba(255, 255, 255, 0.08); }\r\n &.ad-dock-bottom { border-top: 1px solid rgba(255, 255, 255, 0.08); }\r\n\r\n // 상/하 도킹은 세로가 짧으므로 목록을 가로로 흘린다\r\n &.ad-dock-top,\r\n &.ad-dock-bottom {\r\n .ad-list {\r\n display: flex;\r\n flex-wrap: wrap;\r\n align-content: flex-start;\r\n gap: 0 6px;\r\n overflow-y: auto;\r\n }\r\n\r\n .ad-tab {\r\n flex: 0 0 220px;\r\n }\r\n }\r\n\r\n .ad-head {\r\n display: flex;\r\n align-items: center;\r\n justify-content: space-between;\r\n padding: 10px 12px 8px;\r\n letter-spacing: 0.12em;\r\n font-size: 10px;\r\n font-weight: 700;\r\n color: rgba(255, 255, 255, 0.4);\r\n // 헤더를 잡아 끌면 도킹 방향을 바꾼다 (OS 창 이동에 뺏기면 안 되므로 no-drag)\r\n -webkit-app-region: no-drag;\r\n cursor: grab;\r\n\r\n &:active {\r\n cursor: grabbing;\r\n }\r\n }\r\n\r\n // 경계선 — 잡아 끌면 크기가 바뀐다. 붙어 있는 쪽 반대편 가장자리에 놓는다\r\n .ad-resize {\r\n position: absolute;\r\n z-index: 6;\r\n background: transparent;\r\n transition: background 0.12s ease;\r\n\r\n &:hover,\r\n body.ad-resizing & {\r\n background: rgba(97, 175, 239, 0.5);\r\n }\r\n }\r\n\r\n .ad-resize-right,\r\n .ad-resize-left {\r\n top: 0;\r\n bottom: 0;\r\n width: 6px;\r\n cursor: col-resize;\r\n }\r\n\r\n .ad-resize-right { left: 0; }\r\n .ad-resize-left { right: 0; }\r\n\r\n .ad-resize-top,\r\n .ad-resize-bottom {\r\n left: 0;\r\n right: 0;\r\n height: 6px;\r\n cursor: row-resize;\r\n }\r\n\r\n .ad-resize-bottom { top: 0; }\r\n .ad-resize-top { bottom: 0; }\r\n\r\n .ad-head-count {\r\n -webkit-app-region: no-drag;\r\n padding: 1px 6px;\r\n border-radius: 8px;\r\n background: rgba(255, 255, 255, 0.08);\r\n color: rgba(255, 255, 255, 0.55);\r\n }\r\n\r\n // 요구 2-1: 탭이 많아지면 이 영역만 스크롤된다\r\n .ad-list {\r\n flex: 1 1 auto;\r\n overflow-y: auto;\r\n overflow-x: hidden;\r\n padding: 2px 6px 8px;\r\n\r\n &::-webkit-scrollbar {\r\n width: 8px;\r\n }\r\n\r\n &::-webkit-scrollbar-thumb {\r\n background: rgba(255, 255, 255, 0.14);\r\n border-radius: 4px;\r\n\r\n &:hover {\r\n background: rgba(255, 255, 255, 0.24);\r\n }\r\n }\r\n }\r\n\r\n .ad-tab {\r\n position: relative;\r\n display: flex;\r\n align-items: stretch;\r\n gap: 8px;\r\n margin-bottom: 3px;\r\n padding: 7px 6px 7px 0;\r\n border-radius: 5px;\r\n cursor: pointer;\r\n transition: background 0.1s ease;\r\n\r\n &:hover {\r\n background: rgba(255, 255, 255, 0.05);\r\n\r\n .ad-close {\r\n opacity: 0.65;\r\n }\r\n }\r\n\r\n &.active {\r\n background: rgba(255, 255, 255, 0.1);\r\n\r\n .ad-title {\r\n color: #fff;\r\n }\r\n }\r\n }\r\n\r\n // 상태색 표시 바\r\n .ad-bar {\r\n flex: 0 0 3px;\r\n border-radius: 2px;\r\n background: var(--ad-color, #6b727d);\r\n }\r\n\r\n .ad-body {\r\n flex: 1 1 auto;\r\n min-width: 0;\r\n }\r\n\r\n .ad-title {\r\n overflow: hidden;\r\n text-overflow: ellipsis;\r\n white-space: nowrap;\r\n color: rgba(255, 255, 255, 0.82);\r\n font-size: 12.5px;\r\n line-height: 1.35;\r\n }\r\n\r\n .ad-meta {\r\n display: flex;\r\n align-items: center;\r\n gap: 6px;\r\n margin-top: 2px;\r\n font-size: 10.5px;\r\n line-height: 1.4;\r\n color: rgba(255, 255, 255, 0.45);\r\n }\r\n\r\n .ad-badge {\r\n flex: 0 0 auto;\r\n color: var(--ad-color, #6b727d);\r\n font-weight: 600;\r\n\r\n // 수동/훅으로 고정된 상태는 밑줄로 구분\r\n &.pinned {\r\n text-decoration: underline dotted;\r\n text-underline-offset: 2px;\r\n }\r\n }\r\n\r\n // 두 번째 줄 — 마지막으로 보낸 프롬프트\r\n .ad-prompt {\r\n display: flex;\r\n align-items: center;\r\n margin-top: 2px;\r\n min-width: 0;\r\n font-size: 11px;\r\n line-height: 1.4;\r\n }\r\n\r\n .ad-label {\r\n flex: 1 1 auto;\r\n overflow: hidden;\r\n text-overflow: ellipsis;\r\n white-space: nowrap;\r\n color: rgba(255, 255, 255, 0.6);\r\n\r\n // 아직 프롬프트가 없는 탭 — 자리는 지키되 눈에 덜 걸리게\r\n &.empty {\r\n color: rgba(255, 255, 255, 0.25);\r\n }\r\n }\r\n\r\n .ad-elapsed {\r\n flex: 0 0 auto;\r\n margin-left: auto;\r\n font-variant-numeric: tabular-nums;\r\n opacity: 0.55;\r\n }\r\n\r\n .ad-label-input {\r\n flex: 1 1 auto;\r\n min-width: 0;\r\n padding: 0 4px;\r\n border: 1px solid rgba(255, 255, 255, 0.25);\r\n border-radius: 3px;\r\n background: rgba(0, 0, 0, 0.35);\r\n color: #fff;\r\n font-size: 10.5px;\r\n outline: none;\r\n\r\n &:focus {\r\n border-color: var(--ad-color, #61afef);\r\n }\r\n }\r\n\r\n .ad-close {\r\n flex: 0 0 auto;\r\n align-self: center;\r\n width: 18px;\r\n height: 18px;\r\n padding: 0;\r\n border: 0;\r\n border-radius: 3px;\r\n opacity: 0;\r\n background: transparent;\r\n color: rgba(255, 255, 255, 0.8);\r\n font-size: 15px;\r\n line-height: 1;\r\n cursor: pointer;\r\n transition: opacity 0.1s ease, background 0.1s ease;\r\n\r\n &:hover {\r\n opacity: 1 !important;\r\n background: rgba(255, 80, 80, 0.35);\r\n }\r\n }\r\n\r\n .ad-foot {\r\n display: flex;\r\n gap: 6px;\r\n padding: 8px;\r\n border-top: 1px solid rgba(255, 255, 255, 0.07);\r\n }\r\n\r\n .ad-btn {\r\n flex: 1 1 auto;\r\n padding: 5px 8px;\r\n border: 1px solid rgba(255, 255, 255, 0.12);\r\n border-radius: 4px;\r\n background: transparent;\r\n color: rgba(255, 255, 255, 0.6);\r\n font-size: 11px;\r\n cursor: pointer;\r\n\r\n &:hover {\r\n background: rgba(255, 255, 255, 0.08);\r\n color: #fff;\r\n }\r\n }\r\n\r\n // 복구 버튼은 글자가 한 자뿐이라 나머지 버튼과 같은 비율로 늘어나면 허전하다\r\n .ad-repair {\r\n flex: 0 0 auto;\r\n min-width: 28px;\r\n font-size: 13px;\r\n line-height: 1;\r\n }\r\n}\r\n\r\n// 우클릭 상태 메뉴\r\n.ad-menu {\r\n position: fixed;\r\n z-index: 10000;\r\n min-width: 130px;\r\n padding: 4px;\r\n border: 1px solid rgba(255, 255, 255, 0.12);\r\n border-radius: 6px;\r\n background: #262a31;\r\n box-shadow: 0 6px 24px rgba(0, 0, 0, 0.5);\r\n font-size: 12px;\r\n user-select: none;\r\n\r\n .ad-menu-item {\r\n padding: 6px 10px;\r\n border-radius: 4px;\r\n color: rgba(255, 255, 255, 0.75);\r\n cursor: pointer;\r\n\r\n &:hover {\r\n background: rgba(255, 255, 255, 0.1);\r\n color: #fff;\r\n }\r\n }\r\n}\r\n\r\n// 도킹 가이드 — 헤더를 끌고 있는 동안만 창 위에 뜬다\r\n.ad-dock-overlay {\r\n position: fixed;\r\n inset: 0;\r\n z-index: 9998;\r\n // 커서 판정은 좌표로 직접 하므로 오버레이가 이벤트를 가로채면 안 된다\r\n pointer-events: none;\r\n}\r\n\r\n// 놓으면 사이드바가 자리잡을 영역\r\n.ad-dock-preview {\r\n position: fixed;\r\n display: none;\r\n border: 1px solid rgba(97, 175, 239, 0.9);\r\n background: rgba(97, 175, 239, 0.22);\r\n box-shadow: 0 0 0 9999px rgba(0, 0, 0, 0.18);\r\n transition: left 0.08s ease, top 0.08s ease, width 0.08s ease, height 0.08s ease;\r\n}\r\n\r\n// 네 가장자리에 뜨는 도킹 표적\r\n.ad-dock-zone {\r\n position: fixed;\r\n width: 46px;\r\n height: 46px;\r\n margin: -23px 0 0 -23px;\r\n border: 1px solid rgba(255, 255, 255, 0.35);\r\n border-radius: 6px;\r\n background: rgba(38, 42, 49, 0.92);\r\n box-shadow: 0 4px 16px rgba(0, 0, 0, 0.45);\r\n display: flex;\r\n align-items: center;\r\n justify-content: center;\r\n\r\n .ad-dock-mark {\r\n width: 26px;\r\n height: 26px;\r\n border: 1px solid rgba(255, 255, 255, 0.45);\r\n border-radius: 3px;\r\n }\r\n\r\n &.active {\r\n border-color: rgba(97, 175, 239, 0.95);\r\n background: rgba(97, 175, 239, 0.35);\r\n }\r\n\r\n // 표적 안의 작은 사각형이 \"어느 쪽 절반에 붙는지\" 를 그대로 보여준다\r\n &.ad-dock-zone-left .ad-dock-mark { border-left: 4px solid rgba(97, 175, 239, 0.95); }\r\n &.ad-dock-zone-right .ad-dock-mark { border-right: 4px solid rgba(97, 175, 239, 0.95); }\r\n &.ad-dock-zone-top .ad-dock-mark { border-top: 4px solid rgba(97, 175, 239, 0.95); }\r\n &.ad-dock-zone-bottom .ad-dock-mark { border-bottom: 4px solid rgba(97, 175, 239, 0.95); }\r\n}\r\n\r\n// 경계선을 끄는 동안에는 터미널이 커서를 뺏지 않게 한다\r\nbody.ad-resizing {\r\n cursor: inherit;\r\n\r\n .content.main {\r\n pointer-events: none;\r\n }\r\n}\r\n\r\n// AgentDeck 이 켜져 있을 때만 순정 탭바를 감춘다 (사이드바가 그 역할을 대신함)\r\nbody.agentdeck-active {\r\n .content.main > .tab-bar {\r\n display: none !important;\r\n }\r\n\r\n .window {\r\n display: flex;\r\n flex-direction: row;\r\n // 사이드바를 창 안에 절대 배치하므로 기준점이 필요하다\r\n position: relative;\r\n // 터미널 옆에 남는 여백이 밝은 기본 배경으로 튀지 않게 한다\r\n // (--ad-panel-bg 가 있으면 배경 이미지가 이 여백으로도 비친다)\r\n background: var(--ad-panel-bg, var(--bs-body-bg, #1e2127));\r\n }\r\n}\r\n"],"sourceRoot":""}]);
|
|
237
382
|
// Exports
|
|
238
383
|
/* harmony default export */ const __WEBPACK_DEFAULT_EXPORT__ = (___CSS_LOADER_EXPORT___);
|
|
239
384
|
|
|
@@ -700,6 +845,164 @@ const STATUS_STYLES = {
|
|
|
700
845
|
};
|
|
701
846
|
|
|
702
847
|
|
|
848
|
+
/***/ },
|
|
849
|
+
|
|
850
|
+
/***/ "./src/claudeHooks.ts"
|
|
851
|
+
/*!****************************!*\
|
|
852
|
+
!*** ./src/claudeHooks.ts ***!
|
|
853
|
+
\****************************/
|
|
854
|
+
(__unused_webpack_module, __webpack_exports__, __webpack_require__) {
|
|
855
|
+
|
|
856
|
+
__webpack_require__.r(__webpack_exports__);
|
|
857
|
+
/* harmony export */ __webpack_require__.d(__webpack_exports__, {
|
|
858
|
+
/* harmony export */ claudeSettingsPath: () => (/* binding */ claudeSettingsPath),
|
|
859
|
+
/* harmony export */ hooksInstalled: () => (/* binding */ hooksInstalled),
|
|
860
|
+
/* harmony export */ hooksSupported: () => (/* binding */ hooksSupported),
|
|
861
|
+
/* harmony export */ installHooks: () => (/* binding */ installHooks),
|
|
862
|
+
/* harmony export */ isMerged: () => (/* binding */ isMerged),
|
|
863
|
+
/* harmony export */ mergeHooks: () => (/* binding */ mergeHooks),
|
|
864
|
+
/* harmony export */ notifyScriptPath: () => (/* binding */ notifyScriptPath),
|
|
865
|
+
/* harmony export */ stripHooks: () => (/* binding */ stripHooks),
|
|
866
|
+
/* harmony export */ uninstallHooks: () => (/* binding */ uninstallHooks)
|
|
867
|
+
/* harmony export */ });
|
|
868
|
+
/* harmony import */ var fs__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! fs */ "fs");
|
|
869
|
+
/* harmony import */ var fs__WEBPACK_IMPORTED_MODULE_0___default = /*#__PURE__*/__webpack_require__.n(fs__WEBPACK_IMPORTED_MODULE_0__);
|
|
870
|
+
/* harmony import */ var os__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(/*! os */ "os");
|
|
871
|
+
/* harmony import */ var os__WEBPACK_IMPORTED_MODULE_1___default = /*#__PURE__*/__webpack_require__.n(os__WEBPACK_IMPORTED_MODULE_1__);
|
|
872
|
+
/* harmony import */ var path__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(/*! path */ "path");
|
|
873
|
+
/* harmony import */ var path__WEBPACK_IMPORTED_MODULE_2___default = /*#__PURE__*/__webpack_require__.n(path__WEBPACK_IMPORTED_MODULE_2__);
|
|
874
|
+
|
|
875
|
+
|
|
876
|
+
|
|
877
|
+
/**
|
|
878
|
+
* Claude Code 상태 통보 훅을 사용자의 Claude 설정에 등록/해제한다.
|
|
879
|
+
*
|
|
880
|
+
* 왜 플러그인이 남의 설정 파일을 만지나 — 훅은 Claude Code 쪽 설정(`~/.claude/settings.json`)에
|
|
881
|
+
* 있어야 하고, npm 으로 배포되는 Tabby 플러그인은 그 파일에 손댈 방법이 달리 없다.
|
|
882
|
+
* 그래서 "설치할지" 를 사용자가 설정 창에서 직접 고르게 하고, 고른 순간에만 쓴다.
|
|
883
|
+
* 쓰기 전에 항상 백업을 남기고, 남의 훅은 건드리지 않는다.
|
|
884
|
+
*
|
|
885
|
+
* 훅이 있으면 상태 판정이 정확해진다. 에이전트가 "지시를 받았다 / 승인을 기다린다 / 끝났다" 를
|
|
886
|
+
* 직접 알려주기 때문이다. 훅이 없으면 화면에 그려진 글자로 추측하는 폴백(detect.ts)이 도는데,
|
|
887
|
+
* 그건 TUI 가 화면을 어떻게 그리는지에 기대는 만큼 흔들린다.
|
|
888
|
+
*/
|
|
889
|
+
/** 우리 훅인지 알아보는 표식 — 파일 이름이 그대로 명령줄에 들어간다 */
|
|
890
|
+
const MARK = 'agentdeck-notify';
|
|
891
|
+
/**
|
|
892
|
+
* 어떤 순간에 무엇을 보고할지.
|
|
893
|
+
*
|
|
894
|
+
* **상태만 보낸다 — 작업 이름은 여기서 보내지 않는다.** 라벨은 플러그인이 Enter 시점에
|
|
895
|
+
* 입력창을 읽어 매번 갱신하는 경로(`claimEnterLabel`)가 전담한다. 예전에는 여기에
|
|
896
|
+
* `-PromptAsLabel` 이 붙어 있었는데, 훅이 상태 파일의 직전 라벨을 도로 써 넣는 바람에
|
|
897
|
+
* 플러그인이 방금 넣은 최신 프롬프트를 옛 값으로 되돌렸다 (2026-09-01 실측).
|
|
898
|
+
*/
|
|
899
|
+
const PLAN = [
|
|
900
|
+
// 지시를 받았다
|
|
901
|
+
{ event: 'UserPromptSubmit', args: '-Status running' },
|
|
902
|
+
// 승인을 기다린다
|
|
903
|
+
{ event: 'Notification', args: '-Status waiting' },
|
|
904
|
+
// 답이 끝났다
|
|
905
|
+
{ event: 'Stop', args: '-Status done' },
|
|
906
|
+
];
|
|
907
|
+
function claudeSettingsPath() {
|
|
908
|
+
return path__WEBPACK_IMPORTED_MODULE_2__.join(os__WEBPACK_IMPORTED_MODULE_1__.homedir(), '.claude', 'settings.json');
|
|
909
|
+
}
|
|
910
|
+
/** 훅 스크립트의 실제 경로. dist/index.js 기준으로 한 단계 위가 패키지 뿌리다 */
|
|
911
|
+
function notifyScriptPath() {
|
|
912
|
+
return path__WEBPACK_IMPORTED_MODULE_2__.join(__dirname, '..', 'hooks', 'agentdeck-notify.ps1');
|
|
913
|
+
}
|
|
914
|
+
/** 이 환경에서 훅을 걸 수 있나 — 통보 스크립트가 PowerShell 이라 Windows 전용이다 */
|
|
915
|
+
function hooksSupported() {
|
|
916
|
+
return process.platform === 'win32' && fs__WEBPACK_IMPORTED_MODULE_0__.existsSync(notifyScriptPath());
|
|
917
|
+
}
|
|
918
|
+
function readSettings() {
|
|
919
|
+
try {
|
|
920
|
+
const raw = fs__WEBPACK_IMPORTED_MODULE_0__.readFileSync(claudeSettingsPath(), 'utf8');
|
|
921
|
+
return raw.trim() ? JSON.parse(raw) : {};
|
|
922
|
+
}
|
|
923
|
+
catch {
|
|
924
|
+
// 파일이 없거나 아직 JSON 이 아니다 — 새로 만드는 것으로 본다
|
|
925
|
+
return {};
|
|
926
|
+
}
|
|
927
|
+
}
|
|
928
|
+
/** 이 이벤트에 걸린 훅 중 우리 것이 아닌 것만 남긴다 */
|
|
929
|
+
function withoutOurs(entries) {
|
|
930
|
+
return (Array.isArray(entries) ? entries : [])
|
|
931
|
+
.filter(e => !JSON.stringify(e !== null && e !== void 0 ? e : '').includes(MARK));
|
|
932
|
+
}
|
|
933
|
+
/**
|
|
934
|
+
* 우리 훅이 세 이벤트에 모두 걸려 있나 — 파일을 읽지 않는 순수 판정.
|
|
935
|
+
* 파일 접근과 분리해 둔 이유는 이 판단을 테스트로 묶어 두기 위해서다.
|
|
936
|
+
*/
|
|
937
|
+
function isMerged(settings) {
|
|
938
|
+
var _a;
|
|
939
|
+
const hooks = (_a = settings === null || settings === void 0 ? void 0 : settings.hooks) !== null && _a !== void 0 ? _a : {};
|
|
940
|
+
return PLAN.every(p => {
|
|
941
|
+
const entries = Array.isArray(hooks[p.event]) ? hooks[p.event] : [];
|
|
942
|
+
return entries.some((e) => JSON.stringify(e !== null && e !== void 0 ? e : '').includes(MARK));
|
|
943
|
+
});
|
|
944
|
+
}
|
|
945
|
+
/**
|
|
946
|
+
* 설정 객체에 우리 훅을 더한 새 객체를 돌려준다 (원본은 건드리지 않는다).
|
|
947
|
+
* 이미 우리 것이 있으면 명령줄만 지금 경로로 갈아끼우고, 남이 걸어 둔 훅은 그대로 둔다.
|
|
948
|
+
*/
|
|
949
|
+
function mergeHooks(settings, script) {
|
|
950
|
+
var _a;
|
|
951
|
+
const next = { ...(settings !== null && settings !== void 0 ? settings : {}) };
|
|
952
|
+
next.hooks = { ...((_a = next.hooks) !== null && _a !== void 0 ? _a : {}) };
|
|
953
|
+
for (const p of PLAN) {
|
|
954
|
+
const command = `powershell -NoProfile -ExecutionPolicy Bypass -File "${script}" ${p.args}`;
|
|
955
|
+
next.hooks[p.event] = [
|
|
956
|
+
...withoutOurs(next.hooks[p.event]),
|
|
957
|
+
{ matcher: '*', hooks: [{ type: 'command', command, timeout: 5 }] },
|
|
958
|
+
];
|
|
959
|
+
}
|
|
960
|
+
return next;
|
|
961
|
+
}
|
|
962
|
+
/** 우리 훅만 걷어낸 새 객체. 비어 버린 이벤트 키는 지워서 설정을 깨끗하게 남긴다 */
|
|
963
|
+
function stripHooks(settings) {
|
|
964
|
+
const next = { ...(settings !== null && settings !== void 0 ? settings : {}) };
|
|
965
|
+
if (!next.hooks) {
|
|
966
|
+
return next;
|
|
967
|
+
}
|
|
968
|
+
next.hooks = { ...next.hooks };
|
|
969
|
+
for (const p of PLAN) {
|
|
970
|
+
const kept = withoutOurs(next.hooks[p.event]);
|
|
971
|
+
if (kept.length) {
|
|
972
|
+
next.hooks[p.event] = kept;
|
|
973
|
+
}
|
|
974
|
+
else {
|
|
975
|
+
delete next.hooks[p.event];
|
|
976
|
+
}
|
|
977
|
+
}
|
|
978
|
+
return next;
|
|
979
|
+
}
|
|
980
|
+
function hooksInstalled() {
|
|
981
|
+
return isMerged(readSettings());
|
|
982
|
+
}
|
|
983
|
+
/** 훅을 등록한다 */
|
|
984
|
+
function installHooks() {
|
|
985
|
+
const script = notifyScriptPath();
|
|
986
|
+
if (!fs__WEBPACK_IMPORTED_MODULE_0__.existsSync(script)) {
|
|
987
|
+
throw new Error(`훅 스크립트를 찾지 못했다: ${script}`);
|
|
988
|
+
}
|
|
989
|
+
writeSettings(mergeHooks(readSettings(), script));
|
|
990
|
+
}
|
|
991
|
+
/** 훅을 뗀다 */
|
|
992
|
+
function uninstallHooks() {
|
|
993
|
+
writeSettings(stripHooks(readSettings()));
|
|
994
|
+
}
|
|
995
|
+
function writeSettings(settings) {
|
|
996
|
+
const file = claudeSettingsPath();
|
|
997
|
+
fs__WEBPACK_IMPORTED_MODULE_0__.mkdirSync(path__WEBPACK_IMPORTED_MODULE_2__.dirname(file), { recursive: true });
|
|
998
|
+
// 남의 설정을 고치는 일이라 되돌릴 수 있게 한 벌 남긴다
|
|
999
|
+
if (fs__WEBPACK_IMPORTED_MODULE_0__.existsSync(file)) {
|
|
1000
|
+
fs__WEBPACK_IMPORTED_MODULE_0__.copyFileSync(file, file + '.agentdeck-backup');
|
|
1001
|
+
}
|
|
1002
|
+
fs__WEBPACK_IMPORTED_MODULE_0__.writeFileSync(file, JSON.stringify(settings, null, 2) + '\n', 'utf8');
|
|
1003
|
+
}
|
|
1004
|
+
|
|
1005
|
+
|
|
703
1006
|
/***/ },
|
|
704
1007
|
|
|
705
1008
|
/***/ "./src/config.ts"
|
|
@@ -710,11 +1013,31 @@ const STATUS_STYLES = {
|
|
|
710
1013
|
|
|
711
1014
|
__webpack_require__.r(__webpack_exports__);
|
|
712
1015
|
/* harmony export */ __webpack_require__.d(__webpack_exports__, {
|
|
713
|
-
/* harmony export */ AgentDeckConfigProvider: () => (/* binding */ AgentDeckConfigProvider)
|
|
1016
|
+
/* harmony export */ AgentDeckConfigProvider: () => (/* binding */ AgentDeckConfigProvider),
|
|
1017
|
+
/* harmony export */ LAYOUT_DEFAULTS: () => (/* binding */ LAYOUT_DEFAULTS)
|
|
714
1018
|
/* harmony export */ });
|
|
715
1019
|
/* harmony import */ var tabby_core__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! tabby-core */ "tabby-core");
|
|
716
1020
|
/* harmony import */ var tabby_core__WEBPACK_IMPORTED_MODULE_0___default = /*#__PURE__*/__webpack_require__.n(tabby_core__WEBPACK_IMPORTED_MODULE_0__);
|
|
717
1021
|
|
|
1022
|
+
/**
|
|
1023
|
+
* 마우스로 만진 배치만 따로 모아 둔다 — 설정 탭의 "배치 초기화" 가 이 값으로 되돌린다.
|
|
1024
|
+
* 기본값 정의와 초기화 대상이 어긋나지 않도록 아래 defaults 도 이걸 펼쳐 쓴다.
|
|
1025
|
+
*/
|
|
1026
|
+
const LAYOUT_DEFAULTS = {
|
|
1027
|
+
/**
|
|
1028
|
+
* 사이드바가 붙는 방향. 헤더를 잡고 창 가장자리로 끌어다 놓으면 바뀐다.
|
|
1029
|
+
* 'left' | 'right' | 'top' | 'bottom'
|
|
1030
|
+
*/
|
|
1031
|
+
sidebarDock: 'right',
|
|
1032
|
+
/**
|
|
1033
|
+
* 좌/우 도킹일 때의 사이드바 폭 (px). 0 이면 자동 —
|
|
1034
|
+
* 화면비(aspectW:aspectH)로 터미널 폭을 역산하고 남는 만큼을 사이드바가 갖는다.
|
|
1035
|
+
* 경계선을 드래그하는 순간 그 폭이 여기 박히고, 그 뒤로는 자동 계산을 쓰지 않는다.
|
|
1036
|
+
*/
|
|
1037
|
+
sidebarWidth: 0,
|
|
1038
|
+
/** 상/하 도킹일 때의 사이드바 높이 (px). 경계선 드래그로 바뀐다 */
|
|
1039
|
+
sidebarHeight: 200,
|
|
1040
|
+
};
|
|
718
1041
|
/** @hidden */
|
|
719
1042
|
class AgentDeckConfigProvider extends tabby_core__WEBPACK_IMPORTED_MODULE_0__.ConfigProvider {
|
|
720
1043
|
constructor() {
|
|
@@ -733,6 +1056,8 @@ class AgentDeckConfigProvider extends tabby_core__WEBPACK_IMPORTED_MODULE_0__.Co
|
|
|
733
1056
|
*/
|
|
734
1057
|
sidebarMin: 200,
|
|
735
1058
|
sidebarMax: 560,
|
|
1059
|
+
// 마우스로 만지는 배치 (도킹 방향 / 폭 / 높이) — 정의는 LAYOUT_DEFAULTS 에 있다
|
|
1060
|
+
...LAYOUT_DEFAULTS,
|
|
736
1061
|
/**
|
|
737
1062
|
* 훅이 상태를 밀어 넣는 TCP 채널의 포트 (127.0.0.1 전용).
|
|
738
1063
|
*
|
|
@@ -745,11 +1070,20 @@ class AgentDeckConfigProvider extends tabby_core__WEBPACK_IMPORTED_MODULE_0__.Co
|
|
|
745
1070
|
/**
|
|
746
1071
|
* 외부 통보 채널(TCP + 상태 파일 폴링)을 열지.
|
|
747
1072
|
*
|
|
748
|
-
*
|
|
749
|
-
*
|
|
750
|
-
*
|
|
1073
|
+
* 기본으로 켠다. 에이전트 훅이 보내 주는 상태가 가장 정확한 원천이기 때문이다 —
|
|
1074
|
+
* 출력 패턴으로 추측하는 경로(detect.ts)는 훅이 없는 환경을 위한 폴백일 뿐이고,
|
|
1075
|
+
* TUI 가 화면을 어떻게 그리는지에 기대는 만큼 판정이 흔들린다
|
|
1076
|
+
* (2026-09-01: Claude Code 가 놀 때도 상태줄을 매초 갱신해 진행중이 안 풀렸다).
|
|
1077
|
+
* 훅을 안 쓰더라도 비용은 유휴 포트 하나와 400ms 폴링뿐이다.
|
|
1078
|
+
*/
|
|
1079
|
+
notifyChannel: true,
|
|
1080
|
+
/**
|
|
1081
|
+
* 훅 설치 안내를 다시 띄우지 않는다.
|
|
1082
|
+
*
|
|
1083
|
+
* 기동할 때 훅이 안 걸려 있으면 한 번 물어보는데, "다시 묻지 않기" 를 고르면 켜진다.
|
|
1084
|
+
* 설정 창의 설치 버튼은 이 값과 무관하게 언제든 쓸 수 있다.
|
|
751
1085
|
*/
|
|
752
|
-
|
|
1086
|
+
hookPromptDismissed: false,
|
|
753
1087
|
/** PTY 출력이 이 시간(ms) 이상 없으면 running -> idle */
|
|
754
1088
|
idleAfterMs: 2500,
|
|
755
1089
|
/** 출력 패턴으로 상태를 자동 추론할지 */
|
|
@@ -765,15 +1099,8 @@ class AgentDeckConfigProvider extends tabby_core__WEBPACK_IMPORTED_MODULE_0__.Co
|
|
|
765
1099
|
* (사이드바 DOM 갱신뿐이라 미미하다).
|
|
766
1100
|
*/
|
|
767
1101
|
keepRenderingWhenHidden: true,
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
* Claude Code 는 세션 시작 때 붙인 콘솔 제목을 계속 들고 있어서 다음 작업으로
|
|
771
|
-
* 넘어가도 안 바뀐다(2026-08-28 실측) — 제목만 보여주면 사이드바가 옛 작업 이름에
|
|
772
|
-
* 박혀 있게 된다. 라벨이 없는 탭은 그대로 제목을 보여준다.
|
|
773
|
-
* Tabby 의 탭 제목 자체(customTitle)는 건드리지 않는다 — 앱이 넣는 제목을
|
|
774
|
-
* 가리지 않아야 그게 갱신되는지 계속 관측할 수 있다.
|
|
775
|
-
*/
|
|
776
|
-
labelAsTitle: true,
|
|
1102
|
+
// labelAsTitle 은 없앴다 — 사이드바가 제목/프롬프트/상태를 각자 줄에 보여주게 되면서
|
|
1103
|
+
// (deck.service renderTab) 둘 중 하나를 고를 이유가 사라졌다.
|
|
777
1104
|
/**
|
|
778
1105
|
* Enter 로 프롬프트를 보낼 때 그 원문을 작업 라벨로 삼는다 (claimEnterLabel 주석 참고).
|
|
779
1106
|
* 훅도 콘솔 제목도 못 믿는 배포 환경에서 라벨을 최신으로 유지하는 유일한 경로다.
|
|
@@ -837,18 +1164,115 @@ class AgentDeckConfigProvider extends tabby_core__WEBPACK_IMPORTED_MODULE_0__.Co
|
|
|
837
1164
|
/** 사이드바 배경 불투명도 (%). 터미널과 같은 이유로 배경 이미지가 비치게 한다 */
|
|
838
1165
|
sidebarOpacity: 75,
|
|
839
1166
|
/**
|
|
840
|
-
*
|
|
841
|
-
*
|
|
1167
|
+
* tabby-background 의 배경 이미지를 **터미널 영역(`.content.main`) 안에만** 그린다.
|
|
1168
|
+
*
|
|
1169
|
+
* 그 플러그인은 이미지를 `.content-tab-active::before` 에 `position:fixed; 100%x100%`
|
|
1170
|
+
* 로 깔아 창 전체를 덮으므로, 사이드바가 반투명이면 이미지가 사이드바 밑까지 비친다
|
|
1171
|
+
* (2026-09-02 유저 지적: "배경이 agentdeck 사이드바를 침범한다").
|
|
1172
|
+
* true 면 relayout 때마다 터미널 영역 좌표를 CSS 변수(`--ad-bg-*`)로 내려
|
|
1173
|
+
* 그 레이어의 위치·크기를 터미널 영역으로 잘라 붙인다. false 면 순정(창 전체).
|
|
1174
|
+
*/
|
|
1175
|
+
clipBackgroundToTerminal: true,
|
|
1176
|
+
// 크기 보정 설정(`sizeWatchMs` / `sizeWatchIntervalMs` / `redrawKickMs` /
|
|
1177
|
+
// `startupWindowKickMs`)은 그 장치들과 함께 폐지했다 (2026-09-02).
|
|
1178
|
+
// 줄맞춤은 Tabby 순정이 한다 — xterm `ResizeObserver` → `fitAddon.fit()` →
|
|
1179
|
+
// `session.resize()`. agentdeck 은 폭만 정해 `.content.main` 에 적는다.
|
|
1180
|
+
// 자세한 경위는 `deck.service.ts` 의 relayout 주석 참고.
|
|
1181
|
+
/**
|
|
1182
|
+
* 화면 복구(사이드바 `↻` / `agentdeck-repair` 핫키)가 **스크롤백까지 지우고**
|
|
1183
|
+
* 터미널 상태를 초기화한다(`xterm.reset()`).
|
|
1184
|
+
*
|
|
1185
|
+
* true 인 이유: 깨진 화면에는 옛 폭으로 그려진 잔상이 셀에 남아 있는데, 앱이 다시
|
|
1186
|
+
* 그려도 자기가 쓰는 자리만 덮으므로 그 바깥은 지워지지 않는다. 지난 대화를 남기고
|
|
1187
|
+
* 싶으면 false — 그때는 크기 정합 + 흔들기까지만 한다(그것만으로는 안 고쳐지는
|
|
1188
|
+
* 경우가 있다, 2026-09-01 확인).
|
|
1189
|
+
*/
|
|
1190
|
+
/**
|
|
1191
|
+
* 화면이 깨졌는지 **주기적으로 직접 보고**, 깨졌으면 조용히 복구한다.
|
|
1192
|
+
*
|
|
1193
|
+
* 이게 필요한 이유 — 크기 감시(`sizeWatchMs`)는 "우리가 pty 에 보낸 값" 과
|
|
1194
|
+
* "xterm 이 가진 값" 을 비교한다. 그런데 세션이 막 열린 직후의 `session.resize()` 는
|
|
1195
|
+
* **pty 에 도달하지 못하고 유실된다**(2026-09-01 실측: 진단 로그에
|
|
1196
|
+
* `pty resize 254x78 -> 280x78` 을 찍고 `pane.size` 도 280 으로 남겼는데,
|
|
1197
|
+
* 같은 탭에서 셸에 물어보니 `PTYW=254 xtermCols=280`). 보낸 값을 낙관적으로
|
|
1198
|
+
* 기록하기 때문에 감시는 "이제 맞다" 고 보고 손을 뗀다. 그 뒤에 뜨는 TUI 는
|
|
1199
|
+
* 254 폭으로 그리고, 오른쪽 26칸에는 옛 프레임(`Try "how do I …"` 힌트)이 남는다.
|
|
1200
|
+
*
|
|
1201
|
+
* 그래서 판정 기준을 **화면 자체**로 바꾼다. 입력창 테두리 길이가 화면 폭과 다르거나,
|
|
1202
|
+
* 가로선과 입력 텍스트가 한 줄에 섞였거나, 입력창 머리가 둘 보이면 깨진 것이다
|
|
1203
|
+
* (`screen.ts` 의 `judgeScreen` — 정상/깨짐 실측 케이스로 유닛 테스트가 걸려 있다).
|
|
1204
|
+
* 원인이 무엇이든 결과를 보고 고치므로, 원인 하나를 쫓다 다른 것이 깨지는 일이 없다.
|
|
1205
|
+
*/
|
|
1206
|
+
/**
|
|
1207
|
+
* **다시 켬 (2026-09-02).** 껐던 근거가 오진이었다.
|
|
1208
|
+
*
|
|
1209
|
+
* 껐을 때의 이유는 "Claude Code 는 테두리를 `cols - 1` 로 그리는데 판정이
|
|
1210
|
+
* `== cols` 를 정상으로 봐서 멀쩡한 화면을 깨짐으로 친다" 였다. **그 전제가 틀렸다.**
|
|
1211
|
+
* 격리 인스턴스에서 실물 claude 를 띄워 xterm 버퍼를 직접 덤프해 보니
|
|
1212
|
+
* (2026-09-02, cols=271):
|
|
1213
|
+
*
|
|
1214
|
+
* 71 len=271 wrap=0 "────…" ← 테두리는 cols 를 꽉 채운다
|
|
1215
|
+
* 72 len=2 wrap=1 "❯ " ← 꽉 찼기 때문에 autowrap 으로 넘어온 행이다
|
|
1216
|
+
* 73 len=271 wrap=0 "────…"
|
|
1217
|
+
*
|
|
1218
|
+
* 즉 `len == cols` 가 정상이고, `len == cols - 1` 은 **앱이 폭을 1 작게 알고 있는**
|
|
1219
|
+
* 진짜 깨짐이다. 그때는 테두리가 폭을 못 채워 autowrap 이 안 걸리므로 `❯` 가
|
|
1220
|
+
* 윗줄에 붙어버리고 **입력행이 통째로 사라진다**(항목 25 의 ④ 증상이 이것이다).
|
|
1221
|
+
* 껐던 탓에 실사용에서 깨져도 아무도 고치지 않고 방치됐다.
|
|
1222
|
+
*
|
|
1223
|
+
* 예전 자동복구가 화면을 흔들어 놓은 진짜 이유는 판정이 아니라 **조건이 없어서**였다.
|
|
1224
|
+
* 프레임을 그리는 도중을 한 번 잡아도 바로 복구를 때렸다. 그래서 이번에는
|
|
1225
|
+
* `autoRepairMinStreak` 로 "몇 초 넘게 깨진 채로 있을 때만" 손대게 한다.
|
|
842
1226
|
*/
|
|
843
|
-
redrawKickMs: 900,
|
|
844
1227
|
/**
|
|
845
|
-
*
|
|
1228
|
+
* **다시 끔 (2026-09-02, 실사용 실측 4/4 실패).** 위 판정 근거는 여전히 맞지만
|
|
1229
|
+
* **복구 수단이 이 유형에 안 듣는다.** 실사용 로그 06:02:27~06:03:12 —
|
|
1230
|
+
* `redraw nudge 277x78` → `pty resize 276x78 -> 277x78` → `Ctrl+L` 을 다 보내고도
|
|
1231
|
+
* 네 번 모두 `auto-after 아직 깨짐 rule@73 len=276 != cols=277`.
|
|
1232
|
+
* 그동안 10초마다 화면이 흔들려 유저 체감만 나빠졌다("최대화됐다 다시 깨지네").
|
|
846
1233
|
*
|
|
847
|
-
*
|
|
848
|
-
*
|
|
849
|
-
*
|
|
1234
|
+
* 격리에서 한 번에 복구됐던 건 `session.resize(cols-1)` 로 **pty 를 직접 좁혀** 만든
|
|
1235
|
+
* 표본이라, 흔들기가 되돌릴 대상이 실제로 있었다. 실사용은 pty 가 이미 277 로 맞고
|
|
1236
|
+
* **앱만 276** 이라 되돌릴 것이 없다 — 항목 27 이 이 유형을 정확히 짚었던 것이고,
|
|
1237
|
+
* 항목 28 의 반전은 그 표본에는 해당하지 않았다.
|
|
1238
|
+
* 되살리려면 "앱이 pty 폭을 다시 읽게 만드는" 수단부터 찾을 것. 감지·채증(`screenWatch`)은 계속 돈다.
|
|
1239
|
+
*/
|
|
1240
|
+
autoRepairScreen: false,
|
|
1241
|
+
/**
|
|
1242
|
+
* 화면을 **보고 기록만** 한다 (복구는 하지 않는다).
|
|
1243
|
+
*
|
|
1244
|
+
* `autoRepairScreen` 을 끄면서 감지·채증까지 같이 꺼진 게 문제였다 —
|
|
1245
|
+
* 실사용에서 깨져도 `~/.agentdeck-screen.log` 에 한 줄도 안 남아,
|
|
1246
|
+
* "고쳤다고 했는데 또 깨진다" 를 확인할 증거가 없었다
|
|
1247
|
+
* (2026-09-02 실측: Tabby 재시작 11:21 이후 기록 0줄, 그 사이 화면은 깨져 있었다).
|
|
1248
|
+
* 그래서 **관측과 조치를 분리한다.** 이 값이 켜져 있으면 깨짐을 판정해
|
|
1249
|
+
* 화면 원문을 남기기만 하고 키는 쏘지 않는다 — 부작용이 없으니 기본으로 켠다.
|
|
1250
|
+
*/
|
|
1251
|
+
screenWatch: true,
|
|
1252
|
+
/** 자동 복구를 한 번 한 뒤 이만큼은 다시 하지 않는다 (ms) */
|
|
1253
|
+
autoRepairCooldownMs: 10000,
|
|
1254
|
+
/**
|
|
1255
|
+
* 깨짐이 이만큼 **연속으로** 보여야 복구한다 (화면 검사는 2초 그물이므로 2 면 2초 이상).
|
|
1256
|
+
* TUI 는 한 프레임을 여러 번의 write 로 그리므로 그 도중을 읽으면 멀쩡한 화면도
|
|
1257
|
+
* 한 번은 깨져 보인다 — 그 중간 프레임에 키를 쏘지 않기 위한 조건이다.
|
|
1258
|
+
*/
|
|
1259
|
+
autoRepairMinStreak: 2,
|
|
1260
|
+
/**
|
|
1261
|
+
* 한 탭에서 자동 복구를 최대 몇 번까지 할지. 판정이 오탐이거나 앱이 정말
|
|
1262
|
+
* 그렇게 그리는 경우에 무한히 흔드는 것을 막는 안전장치다. 0 이면 무제한.
|
|
1263
|
+
*/
|
|
1264
|
+
autoRepairMaxPerTab: 3,
|
|
1265
|
+
/**
|
|
1266
|
+
* 복구 때 `xterm.reset()` 까지 한다 — **스크롤백(지난 대화)이 날아간다.**
|
|
1267
|
+
* 기본은 끔. 잔상은 `clearInputArea`(화면만 비우고 스크롤백은 보존)로 지우므로
|
|
1268
|
+
* 여기까지 갈 일이 거의 없다. 그걸로도 안 풀리는 화면을 만나면 그때 켠다.
|
|
1269
|
+
*/
|
|
1270
|
+
repairHard: false,
|
|
1271
|
+
/**
|
|
1272
|
+
* 복구 마지막에 `Ctrl+L`(0x0C)을 보내 TUI 에게 전체 재렌더를 시킨다.
|
|
1273
|
+
* 입력 중이던 글자는 유지된다. 이걸 다른 뜻으로 쓰는 앱을 만나면 끈다.
|
|
850
1274
|
*/
|
|
851
|
-
|
|
1275
|
+
repairSendRedrawKey: true,
|
|
852
1276
|
/**
|
|
853
1277
|
* 작업 루트 전용 프로필을 만들고 기본 프로필로 삼는다.
|
|
854
1278
|
*
|
|
@@ -877,8 +1301,6 @@ class AgentDeckConfigProvider extends tabby_core__WEBPACK_IMPORTED_MODULE_0__.Co
|
|
|
877
1301
|
COLORTERM: 'truecolor',
|
|
878
1302
|
FORCE_COLOR: '3',
|
|
879
1303
|
},
|
|
880
|
-
/** true 면 탭마다 UAC 승격 (Tabby 자체를 관리자로 띄웠다면 불필요) */
|
|
881
|
-
rootProfileAdmin: false,
|
|
882
1304
|
/** 기본 프로필 지정을 이미 했는지 — 사용자 선택을 매번 덮어쓰지 않기 위한 플래그 */
|
|
883
1305
|
rootProfileClaimed: false,
|
|
884
1306
|
},
|
|
@@ -928,6 +1350,8 @@ __webpack_require__.r(__webpack_exports__);
|
|
|
928
1350
|
/* harmony import */ var _detect__WEBPACK_IMPORTED_MODULE_7__ = __webpack_require__(/*! ./detect */ "./src/detect.ts");
|
|
929
1351
|
/* harmony import */ var _api__WEBPACK_IMPORTED_MODULE_8__ = __webpack_require__(/*! ./api */ "./src/api.ts");
|
|
930
1352
|
/* harmony import */ var _prompt__WEBPACK_IMPORTED_MODULE_9__ = __webpack_require__(/*! ./prompt */ "./src/prompt.ts");
|
|
1353
|
+
/* harmony import */ var _screen__WEBPACK_IMPORTED_MODULE_10__ = __webpack_require__(/*! ./screen */ "./src/screen.ts");
|
|
1354
|
+
/* harmony import */ var _dock__WEBPACK_IMPORTED_MODULE_11__ = __webpack_require__(/*! ./dock */ "./src/dock.ts");
|
|
931
1355
|
var __decorate = (undefined && undefined.__decorate) || function (decorators, target, key, desc) {
|
|
932
1356
|
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
933
1357
|
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
|
@@ -947,16 +1371,18 @@ var __metadata = (undefined && undefined.__metadata) || function (k, v) {
|
|
|
947
1371
|
|
|
948
1372
|
|
|
949
1373
|
|
|
1374
|
+
|
|
1375
|
+
|
|
950
1376
|
const SIDEBAR_ID = 'agentdeck-sidebar';
|
|
951
1377
|
const BODY_CLASS = 'agentdeck-active';
|
|
952
|
-
/**
|
|
953
|
-
const
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
const
|
|
1378
|
+
/** 배경 이미지 레이어를 터미널 영역으로 자르는 중일 때 body 에 붙는 클래스 (styles.scss 참조) */
|
|
1379
|
+
const BG_CLIP_CLASS = 'ad-bg-clip';
|
|
1380
|
+
/** 터미널이 이보다 좁아지거나 낮아지지 않게 막는다 — 드래그로 사이드바를 끝까지 밀어도 */
|
|
1381
|
+
const MIN_TERM_W = 320;
|
|
1382
|
+
const MIN_TERM_H = 160;
|
|
1383
|
+
/** 사이드바가 이보다 작아지면 목록을 읽을 수 없다 — 드래그로도 이 밑으로는 안 내려간다 */
|
|
1384
|
+
const MIN_SIDEBAR_W = 140;
|
|
1385
|
+
const MIN_SIDEBAR_H = 90;
|
|
960
1386
|
/** Ctrl+V 를 눌렀을 때 xterm 의 네이티브 paste 가 오는지 기다려보는 시간 */
|
|
961
1387
|
const PASTE_NATIVE_WAIT_MS = 60;
|
|
962
1388
|
/**
|
|
@@ -971,8 +1397,56 @@ const PASTE_DEDUPE_MS = 80;
|
|
|
971
1397
|
const PASTE_PROBE_MS = 5000;
|
|
972
1398
|
/** 입력창을 찾으려고 화면 아래에서 훑어 올라갈 줄 수 — 여러 줄 프롬프트를 덮을 만큼만 */
|
|
973
1399
|
const PROMPT_SCAN_ROWS = 14;
|
|
1400
|
+
/**
|
|
1401
|
+
* 복구할 때 Ctrl+L 을 보내기까지 기다리는 시간.
|
|
1402
|
+
* 앞선 흔들기(`nudgePtyRedraw`)가 60ms 뒤에 폭을 되돌리므로, 앱이 그 두 번째 SIGWINCH 로
|
|
1403
|
+
* 새 폭을 받아들인 다음에 "다시 그려라" 가 도착해야 한다. 순서가 뒤집히면 옛 폭으로 다시 그린다.
|
|
1404
|
+
*/
|
|
1405
|
+
const REPAIR_REDRAW_DELAY_MS = 180;
|
|
1406
|
+
/** 복구 뒤 화면을 다시 재는 시각 — 앱이 새 프레임을 다 그릴 만큼은 기다린다 */
|
|
1407
|
+
const SNAP_AFTER_MS = 1500;
|
|
974
1408
|
/** 진단 로그 — devtools 를 못 여는 상황에서도 밖에서 읽을 수 있게 파일로 남긴다 */
|
|
1409
|
+
/**
|
|
1410
|
+
* 화면 깨짐 검사를 출력이 멈춘 뒤 이만큼 기다렸다 한다.
|
|
1411
|
+
* TUI 가 한 프레임을 여러 번의 write 로 그리므로, 그리는 도중에 읽으면 멀쩡한 화면도 깨져 보인다.
|
|
1412
|
+
*/
|
|
1413
|
+
const SCREEN_CHECK_DEBOUNCE_MS = 700;
|
|
1414
|
+
/**
|
|
1415
|
+
* pty 크기를 고친 뒤 같은 값을 다시 보내는 시점 (ms).
|
|
1416
|
+
* 세션 초기화 직후의 resize 는 유실되므로(syncPtySize 주석) 시간을 벌려 몇 번 더 던진다.
|
|
1417
|
+
*/
|
|
1418
|
+
const PTY_RESIZE_RETRY_MS = [150, 400, 900, 1800, 3000];
|
|
1419
|
+
/**
|
|
1420
|
+
* 탭이 열린 뒤 "지금 xterm 이 쓰는 크기" 를 pty 에 알려 주는 시점 (ms).
|
|
1421
|
+
*
|
|
1422
|
+
* 순정이 못 메우는 구멍이 딱 하나 남아 있다 — **새 탭.** Tabby 는 프론트엔드의 첫 `resize$`
|
|
1423
|
+
* 값으로 세션을 스폰하므로(tabby-local `initializeSession(this.size…)`), 그 스냅샷이
|
|
1424
|
+
* 레이아웃 확정보다 이르면 pty 만 좁게 태어난다. 그 뒤 xterm 이 넓어져도 **크기 변화
|
|
1425
|
+
* 이벤트가 나지 않으므로** ResizeObserver 도 fit() 도 아무것도 보내지 않는다(fit 은 열/행이
|
|
1426
|
+
* 이미 맞으면 no-op). 아무도 pty 에 알려주지 않아 그대로 굳는다.
|
|
1427
|
+
*
|
|
1428
|
+
* 실측(2026-09-02, 격리 인스턴스): 평범하게 연 새 탭 3개가 전부 `xterm=88 / pane.size=88`
|
|
1429
|
+
* 인데 pty 는 72 였고, TUI 는 72 폭으로 그려 테두리 두 줄이 73·88 로 어긋났다.
|
|
1430
|
+
* 같은 값(`session.resize(88, 39)`)을 **한 번** 보내자 즉시 정상으로 돌아왔다.
|
|
1431
|
+
*
|
|
1432
|
+
* 그래서 되살린 개입은 이것 하나뿐이다 — "현재 크기를 몇 번 알려준다". 흔들기(cols-1 ↔ cols)도,
|
|
1433
|
+
* 250ms 감시 루프도, fit 강제도 아니다. 값이 이미 맞으면 SIGWINCH 가 나지 않아 무해하고,
|
|
1434
|
+
* xterm 을 건드리지 않으므로 그리는 중인 프레임과 경쟁하지도 않는다.
|
|
1435
|
+
*/
|
|
1436
|
+
const TAB_OPEN_ANNOUNCE_MS = [250, 800, 1800];
|
|
1437
|
+
/** 화면 깨짐 주기 검사 간격 (ms) — 출력 구독이 없는 탭도 잡기 위한 그물 */
|
|
1438
|
+
const SCREEN_SWEEP_MS = 2000;
|
|
1439
|
+
/** 자식이 아직 없는 래퍼 탭을 다시 훑는 간격과 횟수 */
|
|
1440
|
+
const EMPTY_TAB_RETRY_MS = 400;
|
|
1441
|
+
const EMPTY_TAB_RETRY_MAX = 20;
|
|
975
1442
|
const DIAG_PATH = path__WEBPACK_IMPORTED_MODULE_5__.join(os__WEBPACK_IMPORTED_MODULE_4__.homedir(), '.agentdeck-diag.log');
|
|
1443
|
+
/**
|
|
1444
|
+
* 깨진 화면의 **원문**을 남기는 곳. 진단 로그(한 줄 요약)와 따로 두는 이유 —
|
|
1445
|
+
* 원인을 못 찾는 이유가 늘 "깨졌을 때의 화면을 아무도 안 봐 뒀다" 여서다.
|
|
1446
|
+
* 감지 직후(before)와 복구 뒤(after)를 같이 남기므로, 다음에 깨지면 추측 없이
|
|
1447
|
+
* "무엇이 깨졌나 / 복구가 실제로 고쳤나" 를 이 파일 하나로 가른다.
|
|
1448
|
+
*/
|
|
1449
|
+
const SNAP_PATH = path__WEBPACK_IMPORTED_MODULE_5__.join(os__WEBPACK_IMPORTED_MODULE_4__.homedir(), '.agentdeck-screen.log');
|
|
976
1450
|
let cachedClipboard;
|
|
977
1451
|
/**
|
|
978
1452
|
* Electron 의 clipboard 를 런타임에 가져온다.
|
|
@@ -1059,16 +1533,24 @@ let AgentDeckService = class AgentDeckService {
|
|
|
1059
1533
|
/** 직전에 적용한 폭 — 같은 값이면 DOM 을 다시 건드리지 않는다 */
|
|
1060
1534
|
this.lastTermW = -1;
|
|
1061
1535
|
this.lastSidebarW = -1;
|
|
1536
|
+
/** 직전에 적용한 도킹 방향 — 크기가 같아도 방향이 바뀌면 다시 그려야 한다 */
|
|
1537
|
+
this.lastDock = null;
|
|
1538
|
+
/** 경계선 드래그(크기)와 헤더 드래그(도킹 방향)를 맡는다 */
|
|
1539
|
+
this.dock = null;
|
|
1062
1540
|
this.refitQueued = false;
|
|
1063
1541
|
/** 이미 감시를 건 래퍼 탭 / pane — 중복 구독 방지 */
|
|
1064
1542
|
this.watched = new WeakSet();
|
|
1065
1543
|
this.watchedPanes = new WeakSet();
|
|
1066
|
-
/**
|
|
1067
|
-
this.
|
|
1068
|
-
/**
|
|
1069
|
-
this.
|
|
1070
|
-
/**
|
|
1071
|
-
this.
|
|
1544
|
+
/** 화면 깨짐 검사 예약 타이머 — pane 당 하나만 돈다 */
|
|
1545
|
+
this.screenCheckTimers = new WeakMap();
|
|
1546
|
+
/** 빈 래퍼 탭을 다시 훑은 횟수 */
|
|
1547
|
+
this.emptyTabRetries = new WeakMap();
|
|
1548
|
+
/** 마지막 화면 판정 결과 — 상태가 바뀔 때만 채증하려고 들고 있는다 */
|
|
1549
|
+
this.screenObs = new WeakMap();
|
|
1550
|
+
/** 마지막 자동 복구 시각 — 쿨다운 판정용 */
|
|
1551
|
+
this.autoRepairAt = new WeakMap();
|
|
1552
|
+
/** 자동 복구 횟수 — 오탐으로 무한히 흔드는 것을 막는다 */
|
|
1553
|
+
this.autoRepairCount = new WeakMap();
|
|
1072
1554
|
/** 진단 기록 — devtools 의 `__agentdeck.kickLog` 와 DIAG_PATH 파일에 함께 남는다 */
|
|
1073
1555
|
this.kickLog = [];
|
|
1074
1556
|
/** diagOnce 가 이미 남긴 키 */
|
|
@@ -1132,7 +1614,6 @@ let AgentDeckService = class AgentDeckService {
|
|
|
1132
1614
|
catch {
|
|
1133
1615
|
// 진단 파일을 못 열어도 그냥 간다
|
|
1134
1616
|
}
|
|
1135
|
-
this.setupAt = Date.now();
|
|
1136
1617
|
this.diag(`${new Date().toISOString()} setup start tabs=${this.app.tabs.length}`);
|
|
1137
1618
|
this.buildSidebar();
|
|
1138
1619
|
this.applyEnabled();
|
|
@@ -1181,6 +1662,10 @@ let AgentDeckService = class AgentDeckService {
|
|
|
1181
1662
|
this.relayout();
|
|
1182
1663
|
this.scheduleRender();
|
|
1183
1664
|
});
|
|
1665
|
+
// 화면 깨짐 주기 검사. 출력 구독(scheduleScreenCheck)이 더 정밀하지만, 구독이 붙지
|
|
1666
|
+
// 못한 탭에서는 아무도 보지 않게 되므로 그물을 하나 더 둔다. 활성 탭만 본다 —
|
|
1667
|
+
// 숨은 탭은 폭이 0 이라 판정할 근거가 없다.
|
|
1668
|
+
setInterval(() => this.sweepScreens(), SCREEN_SWEEP_MS);
|
|
1184
1669
|
// 창 크기 변화 -> 4:3 폭 재계산
|
|
1185
1670
|
this.observer = new ResizeObserver(() => this.relayout());
|
|
1186
1671
|
this.observer.observe(this.windowEl);
|
|
@@ -1197,10 +1682,12 @@ let AgentDeckService = class AgentDeckService {
|
|
|
1197
1682
|
if (hotkey === 'agentdeck-paste') {
|
|
1198
1683
|
this.doPaste('hotkey');
|
|
1199
1684
|
}
|
|
1685
|
+
if (hotkey === 'agentdeck-repair') {
|
|
1686
|
+
this.repair('all');
|
|
1687
|
+
}
|
|
1200
1688
|
});
|
|
1201
1689
|
// 경과 시간 갱신 + running -> idle 자동 복귀
|
|
1202
1690
|
setInterval(() => this.tick(), 1000);
|
|
1203
|
-
this.scheduleWindowKick();
|
|
1204
1691
|
this.relayout();
|
|
1205
1692
|
this.render();
|
|
1206
1693
|
this.diag(`${new Date().toISOString()} setup done tabs=${this.app.tabs.length} enabled=${this.enabled} mainEl=${!!this.mainEl}`);
|
|
@@ -1210,205 +1697,529 @@ let AgentDeckService = class AgentDeckService {
|
|
|
1210
1697
|
status: this.status,
|
|
1211
1698
|
relayout: () => this.relayout(),
|
|
1212
1699
|
render: () => this.render(),
|
|
1700
|
+
repair: (scope = 'all') => this.repair(scope),
|
|
1213
1701
|
kickLog: this.kickLog,
|
|
1214
1702
|
};
|
|
1215
1703
|
}
|
|
1216
|
-
// ----------
|
|
1704
|
+
// ---------- 화면 복구 ----------
|
|
1217
1705
|
/**
|
|
1218
|
-
*
|
|
1706
|
+
* 깨진 화면을 사람이 직접 되살린다 — 창을 복원→최대화로 흔드는 것을 코드로 대신한다.
|
|
1219
1707
|
*
|
|
1220
|
-
*
|
|
1221
|
-
*
|
|
1222
|
-
*
|
|
1708
|
+
* 자동 경로(`watchSize`)는 "xterm 과 pty 의 크기가 어긋났다" 를 보고 고치는데, 그 감시는
|
|
1709
|
+
* 탭이 열리고 `sizeWatchMs`(기본 6초) 동안만 돈다. 그 뒤에 깨지는 경우가 남는다 —
|
|
1710
|
+
* 두 크기는 서로 맞는데 **TUI 가 들고 있는 프레임만** 옛것인 상황이다. 크기가 이미 같으니
|
|
1711
|
+
* 커널은 SIGWINCH 를 내지 않고, 앱은 다시 그릴 이유를 영영 못 받는다. 증상은
|
|
1712
|
+
* "statusline 이 사라지고 입력창 구분선이 중간에서 끊긴다" (2026-09-01 실측).
|
|
1713
|
+
*
|
|
1714
|
+
* 그래서 복구는 크기를 맞추는 것으로 끝내지 않고 **항상 한 번 흔든다**(`nudgePtyRedraw`).
|
|
1715
|
+
* 어긋남이 없어도 무해하다 — 폭이 한 칸 줄었다 돌아올 뿐이고 xterm 은 건드리지 않는다.
|
|
1716
|
+
*
|
|
1717
|
+
* @param scope `active` = 지금 보고 있는 탭만, `all` = 열려 있는 모든 탭
|
|
1223
1718
|
*/
|
|
1224
|
-
|
|
1225
|
-
const
|
|
1226
|
-
|
|
1227
|
-
this.
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
}
|
|
1242
|
-
// pane 은 래퍼보다 늦게 붙는 경우가 있으므로 호출될 때마다 다시 훑는다
|
|
1243
|
-
// (래퍼만 보고 한 번에 끝내면 목록에 먼저 들어온 빈 SplitTab 을 잡고 끝나버린다)
|
|
1244
|
-
const panes = typeof anyRoot.getAllTabs === 'function'
|
|
1245
|
-
? anyRoot.getAllTabs()
|
|
1246
|
-
: [root];
|
|
1247
|
-
this.diag(`${new Date().toISOString()} watchTab panes=${panes.length}`);
|
|
1248
|
-
for (const pane of panes) {
|
|
1249
|
-
this.watchPane(root, pane);
|
|
1250
|
-
}
|
|
1251
|
-
}
|
|
1252
|
-
watchPane(root, pane) {
|
|
1253
|
-
var _a;
|
|
1254
|
-
if (this.watchedPanes.has(pane)) {
|
|
1255
|
-
return;
|
|
1256
|
-
}
|
|
1257
|
-
this.watchedPanes.add(pane);
|
|
1258
|
-
this.diag(`${new Date().toISOString()} watchPane ${(_a = pane.constructor) === null || _a === void 0 ? void 0 : _a.name}`);
|
|
1259
|
-
if (this.setupAt && Date.now() - this.setupAt < KICK_REARM_MS) {
|
|
1260
|
-
this.startupPanes.push(pane);
|
|
1261
|
-
}
|
|
1262
|
-
const anyPane = pane;
|
|
1263
|
-
const bind = (session) => {
|
|
1264
|
-
if (!session || !session.output$) {
|
|
1265
|
-
return;
|
|
1719
|
+
repair(scope = 'all') {
|
|
1720
|
+
const targets = scope === 'active' && this.app.activeTab
|
|
1721
|
+
? [this.app.activeTab]
|
|
1722
|
+
: this.app.tabs;
|
|
1723
|
+
this.diag(`${new Date().toISOString()} repair scope=${scope} tabs=${targets.length}`);
|
|
1724
|
+
// 1) 사이드바 폭부터 다시 잰다 — 어긋남의 출발점이 대개 여기다
|
|
1725
|
+
this.relayout();
|
|
1726
|
+
// 2) 레이아웃이 실제로 반영된 다음에 재야 한다 (scheduleRefit 과 같은 이유로 두 프레임)
|
|
1727
|
+
requestAnimationFrame(() => requestAnimationFrame(() => {
|
|
1728
|
+
for (const tab of targets) {
|
|
1729
|
+
const anyTab = tab;
|
|
1730
|
+
const panes = typeof anyTab.getAllTabs === 'function'
|
|
1731
|
+
? anyTab.getAllTabs()
|
|
1732
|
+
: [tab];
|
|
1733
|
+
for (const pane of panes) {
|
|
1734
|
+
this.repairPane(pane);
|
|
1735
|
+
}
|
|
1266
1736
|
}
|
|
1267
|
-
|
|
1268
|
-
;(0,_detect__WEBPACK_IMPORTED_MODULE_7__.applyOutput)(this.status, root, pane, data, this.config.store.agentDeck.autoDetect);
|
|
1269
|
-
});
|
|
1270
|
-
};
|
|
1271
|
-
bind(anyPane.session);
|
|
1272
|
-
if (anyPane.sessionChanged$) {
|
|
1273
|
-
anyPane.sessionChanged$.subscribe((session) => bind(session));
|
|
1274
|
-
}
|
|
1275
|
-
// 프론트엔드가 붙는 도중에 우리가 폭을 바꿨으면 그 변경은 유실된다
|
|
1276
|
-
// (Tabby 는 attach() 맨 끝에서야 ResizeObserver 를 건다) — 준비되면 한 번 맞춰준다
|
|
1277
|
-
if (anyPane.frontendReady$) {
|
|
1278
|
-
anyPane.frontendReady$.subscribe(() => {
|
|
1279
|
-
this.scheduleRefit();
|
|
1280
|
-
this.scheduleRedrawKick(pane);
|
|
1281
|
-
});
|
|
1282
|
-
}
|
|
1283
|
-
// frontendReady 는 replay 가 없는 맨 Subject 다 (tabby-terminal 의 `frontendReady: Subject<void>`).
|
|
1284
|
-
// Tabby 를 처음 띄우면 복원된 탭의 프론트엔드가 우리 setup() 보다 먼저 붙어버려서
|
|
1285
|
-
// 위 구독은 이미 지나간 이벤트를 영영 못 받는다 — 그래서 첫 기동에서만 흔들기가 빠졌다.
|
|
1286
|
-
// (새 탭은 tabOpened$ 로 먼저 구독이 걸리니 정상 수신 → 새 탭만 되던 이유)
|
|
1287
|
-
// 이벤트와 무관하게 항상 한 번 예약하고, 아직 준비 전이면 kickPtySize 가 재시도한다.
|
|
1288
|
-
// kickedPanes WeakSet 이 있어 구독 경로와 겹쳐도 흔들기는 pane 당 한 번뿐이다.
|
|
1289
|
-
this.scheduleRedrawKick(pane);
|
|
1737
|
+
}));
|
|
1290
1738
|
}
|
|
1291
1739
|
/**
|
|
1292
|
-
*
|
|
1740
|
+
* 복구 한 pane 분 — **화면을 지우고 앱에게 처음부터 다시 그리게 한다.**
|
|
1293
1741
|
*
|
|
1294
|
-
*
|
|
1295
|
-
*
|
|
1296
|
-
*
|
|
1742
|
+
* 흔들기(`nudgePtyRedraw`)만으로는 안 고쳐지는 경우가 실제로 있다(2026-09-01 유저 확인).
|
|
1743
|
+
* 이유는 흔들기가 고치는 대상이 하나뿐이라서다 — 그건 "앱이 폭을 잘못 알고 있다" 를 푼다.
|
|
1744
|
+
* 그런데 깨진 화면에는 그 말고도 두 가지가 더 얹혀 있다.
|
|
1297
1745
|
*
|
|
1298
|
-
*
|
|
1746
|
+
* - **xterm 쪽 잔상**: 옛 폭으로 그려진 글자가 셀에 그대로 남아 있다. 앱이 다시 그려도
|
|
1747
|
+
* 앱은 자기가 쓴 자리만 덮으므로, 그 바깥(옛 프레임의 오른쪽·아래)은 영영 안 지워진다.
|
|
1748
|
+
* - **터미널 상태**: 스크롤 영역(DECSTBM)·줄바꿈 모드·속성이 옛 크기 기준으로 잡혀 있으면
|
|
1749
|
+
* 새로 그린 것까지 어긋난 자리에 들어간다.
|
|
1750
|
+
*
|
|
1751
|
+
* 그래서 순서가 있다 — ① 크기를 맞추고 ② `reset()` 으로 터미널 상태와 화면을 비우고
|
|
1752
|
+
* ③ 그래도 앱은 자기가 이미 그렸다고 믿고 있으니 흔들어 SIGWINCH 를 주고
|
|
1753
|
+
* ④ 마지막으로 `Ctrl+L`(0x0C) 을 보내 "전부 다시 그려라" 를 명시로 시킨다.
|
|
1754
|
+
* ③ 만으로 부족했던 이유가 ①②④ 가 빠져서였다.
|
|
1755
|
+
*
|
|
1756
|
+
* `reset()` 은 스크롤백을 함께 지운다. 지난 대화를 남기고 싶으면 `repairHard: false` —
|
|
1757
|
+
* 그때는 예전처럼 크기 정합 + 흔들기까지만 한다.
|
|
1299
1758
|
*/
|
|
1300
|
-
|
|
1301
|
-
|
|
1302
|
-
|
|
1303
|
-
|
|
1759
|
+
repairPane(pane) {
|
|
1760
|
+
var _a, _b, _c, _d, _e, _f;
|
|
1761
|
+
const cfg = this.config.store.agentDeck;
|
|
1762
|
+
const anyPane = pane;
|
|
1763
|
+
const frontend = anyPane.frontend;
|
|
1764
|
+
const xterm = frontend === null || frontend === void 0 ? void 0 : frontend.xterm;
|
|
1765
|
+
// 숨겨진 탭은 컨테이너 폭이 0 이라 fit 하면 1열로 줄어든다 — 크기는 건드리지 않고
|
|
1766
|
+
// 다시 선택될 때 Tabby 가 재측정하게 둔다
|
|
1767
|
+
const host = (_a = xterm === null || xterm === void 0 ? void 0 : xterm.element) === null || _a === void 0 ? void 0 : _a.parentElement;
|
|
1768
|
+
if (!(host === null || host === void 0 ? void 0 : host.clientWidth) || !(host === null || host === void 0 ? void 0 : host.clientHeight)) {
|
|
1304
1769
|
return;
|
|
1305
1770
|
}
|
|
1306
|
-
this.
|
|
1307
|
-
|
|
1308
|
-
|
|
1771
|
+
const before = this.readScreen(pane);
|
|
1772
|
+
const beforeVerdict = before ? (0,_screen__WEBPACK_IMPORTED_MODULE_10__.judgeScreen)(before.lines, before.cols) : null;
|
|
1773
|
+
this.snapshot(pane, `manual-before ${beforeVerdict
|
|
1774
|
+
? (beforeVerdict.broken ? beforeVerdict.reasons.join(' / ') : '판정=정상')
|
|
1775
|
+
: '읽을 수 없음'}`);
|
|
1776
|
+
// ① 크기 정합 — 여기가 어긋나 있으면 뒤 단계가 전부 헛돈다
|
|
1777
|
+
try {
|
|
1778
|
+
(_c = (_b = frontend === null || frontend === void 0 ? void 0 : frontend.fitAddon) === null || _b === void 0 ? void 0 : _b.fit) === null || _c === void 0 ? void 0 : _c.call(_b);
|
|
1779
|
+
}
|
|
1780
|
+
catch {
|
|
1781
|
+
// 아직 붙는 중이다 — 아래 단계만이라도 태운다
|
|
1782
|
+
}
|
|
1783
|
+
this.syncPtySize(pane, frontend);
|
|
1784
|
+
// ② 잔상 지우기 — 입력창 영역만 비운다 (대화는 남는다)
|
|
1785
|
+
if (before) {
|
|
1786
|
+
this.clearInputArea(pane, before);
|
|
1787
|
+
}
|
|
1788
|
+
// ②-b 그래도 안 되면 터미널 상태까지 초기화 (스크롤백이 날아가므로 기본은 끔)
|
|
1789
|
+
if (cfg.repairHard === true) {
|
|
1309
1790
|
try {
|
|
1310
|
-
|
|
1791
|
+
(_d = xterm === null || xterm === void 0 ? void 0 : xterm.reset) === null || _d === void 0 ? void 0 : _d.call(xterm);
|
|
1311
1792
|
}
|
|
1312
1793
|
catch {
|
|
1313
|
-
//
|
|
1314
|
-
}
|
|
1315
|
-
if (!maximized) {
|
|
1316
|
-
this.diag(`${new Date().toISOString()} window-kick skipped (not maximized)`);
|
|
1317
|
-
return;
|
|
1794
|
+
// 프론트엔드가 막 떨어졌다
|
|
1318
1795
|
}
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
|
|
1796
|
+
}
|
|
1797
|
+
try {
|
|
1798
|
+
(_e = xterm === null || xterm === void 0 ? void 0 : xterm.refresh) === null || _e === void 0 ? void 0 : _e.call(xterm, 0, ((_f = xterm.rows) !== null && _f !== void 0 ? _f : 1) - 1);
|
|
1799
|
+
}
|
|
1800
|
+
catch { }
|
|
1801
|
+
// ③ 크기가 이미 맞아도 무조건 흔든다 — 이 함수를 부르는 이유 자체가 "맞는데 깨졌다" 다
|
|
1802
|
+
this.nudgePtyRedraw(pane);
|
|
1803
|
+
// ④ 앱에게 직접 "다시 그려라". 흔들기가 도착해 앱이 새 폭을 받은 뒤에 보내야 하므로
|
|
1804
|
+
// nudge 의 되돌리기(60ms)보다 확실히 뒤에 둔다.
|
|
1805
|
+
if (cfg.repairSendRedrawKey !== false) {
|
|
1806
|
+
setTimeout(() => this.sendRedrawKey(pane), REPAIR_REDRAW_DELAY_MS);
|
|
1807
|
+
}
|
|
1808
|
+
// ⑤ 채증 — "↻ 를 눌렀는데 안 고쳐진다" 를 감이 아니라 파일로 가른다.
|
|
1809
|
+
// 누른 직후와 다시 그려진 뒤를 짝으로 남긴다.
|
|
1810
|
+
setTimeout(() => {
|
|
1811
|
+
const after = this.readScreen(pane);
|
|
1812
|
+
const still = after ? (0,_screen__WEBPACK_IMPORTED_MODULE_10__.judgeScreen)(after.lines, after.cols) : null;
|
|
1813
|
+
this.snapshot(pane, `manual-after ${still ? (still.broken ? '아직 깨짐: ' + still.reasons.join(' / ') : '복구됨') : '읽을 수 없음'}`);
|
|
1814
|
+
}, SNAP_AFTER_MS);
|
|
1326
1815
|
}
|
|
1327
1816
|
/**
|
|
1328
|
-
*
|
|
1329
|
-
*
|
|
1330
|
-
* 왜 필요한가 — 탭이 만들어지는 동안 우리가 `.content.main` 폭을 바꾸면 그 사이에
|
|
1331
|
-
* 이미 뜬 TUI(Claude Code 등)는 **옛 폭 기준으로 첫 화면을 그려 놓은 상태**가 된다.
|
|
1332
|
-
* 그 뒤 refit/syncPtySize 가 크기를 맞춰도, 맞춘 값이 pty 가 이미 들고 있던 값과 같으면
|
|
1333
|
-
* 커널은 SIGWINCH 를 내지 않는다 — TUI 는 "크기 안 바뀌었네" 하고 그대로 있고,
|
|
1334
|
-
* 옛 폭으로 그린 입력창 프레임 잔상이 화면에 남는다 (2026-08-28 실측: 첫 한 번만 재현,
|
|
1335
|
-
* 창 복원→최대화로 흔들면 즉시 정상 — 그게 SIGWINCH 를 두 번 내기 때문이다).
|
|
1817
|
+
* TUI 에 "화면을 전부 다시 그려라" 를 보낸다 — `Ctrl+L`(0x0C).
|
|
1336
1818
|
*
|
|
1337
|
-
*
|
|
1338
|
-
*
|
|
1339
|
-
* 정상 동작하던 탭에는 재렌더 1회 외에 영향이 없다.
|
|
1819
|
+
* Claude Code·Codex 를 포함해 거의 모든 TUI 와 셸이 이걸 전체 재렌더로 받는다.
|
|
1820
|
+
* 입력 중이던 글자는 건드리지 않는다(readline·TUI 모두 입력 버퍼는 유지하고 화면만 다시 그린다).
|
|
1340
1821
|
*/
|
|
1341
|
-
|
|
1342
|
-
|
|
1822
|
+
sendRedrawKey(pane) {
|
|
1823
|
+
var _a;
|
|
1824
|
+
const anyPane = pane;
|
|
1825
|
+
if (typeof anyPane.sendInput !== 'function' || !((_a = anyPane.session) === null || _a === void 0 ? void 0 : _a.open)) {
|
|
1343
1826
|
return;
|
|
1344
1827
|
}
|
|
1345
|
-
|
|
1346
|
-
|
|
1347
|
-
|
|
1348
|
-
|
|
1828
|
+
this.diag(`${new Date().toISOString()} repair redraw-key ctrl-l`);
|
|
1829
|
+
try {
|
|
1830
|
+
anyPane.sendInput('\x0c');
|
|
1831
|
+
}
|
|
1832
|
+
catch {
|
|
1833
|
+
// 세션이 막 닫혔다
|
|
1349
1834
|
}
|
|
1350
|
-
this.kickedPanes.add(pane);
|
|
1351
|
-
this.diag(`${new Date().toISOString()} scheduled +${delay}ms`);
|
|
1352
|
-
setTimeout(() => this.kickPtySize(pane), delay);
|
|
1353
1835
|
}
|
|
1354
|
-
|
|
1355
|
-
|
|
1836
|
+
/**
|
|
1837
|
+
* 입력창 영역에 남은 **옛 프레임 잔상만** 지운다 (대화 = 스크롤백은 그대로 둔다).
|
|
1838
|
+
*
|
|
1839
|
+
* 복구가 계속 실패하던 진짜 이유가 여기 있었다 (2026-09-02 실측).
|
|
1840
|
+
* `nudge` 도 `Ctrl+L` 도 하는 일은 "앱아 다시 그려라" 뿐인데, **앱은 자기가 쓰는 자리만 덮는다.**
|
|
1841
|
+
* 그래서 옛 폭으로 그려진 줄이 `❯` 자리를 차지하고 있으면 아무도 그걸 못 지운다 —
|
|
1842
|
+
* 채증 원문이 그 모양이었다 (`.agentdeck-screen.log` 06:02:27):
|
|
1843
|
+
*
|
|
1844
|
+
* 72 len=275 " " ← 위 테두리가 있어야 할 자리, 공백
|
|
1845
|
+
* 73 len=276 "────…" ← ❯ 자리에 옛 프레임(276) 잔상
|
|
1846
|
+
* 74 len=277 "────…" ← 앱이 새로 그린 아래 테두리(277, 정상)
|
|
1847
|
+
*
|
|
1848
|
+
* 앱은 이미 277 로 제대로 그리고 있는데 73 행 잔상 때문에 계속 깨짐으로 남았다.
|
|
1849
|
+
* `auto-after 아직 깨짐` 이 두 번 연속 찍힌 게 이것이다.
|
|
1850
|
+
*
|
|
1851
|
+
* 그래서 **터미널 쪽에서 직접 지운다** — 입력창 첫 테두리 행으로 커서를 옮기고
|
|
1852
|
+
* `ESC[J`(커서부터 화면 끝까지)로 비운다. 이건 화면만 비우고 스크롤백은 건드리지 않으므로
|
|
1853
|
+
* `xterm.reset()`(스크롤백까지 날아감)과 다르다. 커서는 `ESC7`/`ESC8` 로 저장·복원해
|
|
1854
|
+
* 앱이 들고 있는 커서 위치와 어긋나지 않게 한다. 지운 뒤 `Ctrl+L` 을 보내면 앱이
|
|
1855
|
+
* 자기 UI 를 그 빈 자리에 다시 그린다.
|
|
1856
|
+
*/
|
|
1857
|
+
clearInputArea(pane, read) {
|
|
1858
|
+
var _a;
|
|
1859
|
+
const xterm = (_a = pane.frontend) === null || _a === void 0 ? void 0 : _a.xterm;
|
|
1860
|
+
if (!xterm || typeof xterm.write !== 'function') {
|
|
1861
|
+
return false;
|
|
1862
|
+
}
|
|
1863
|
+
const lines = read.lines;
|
|
1864
|
+
// 하단 16행 안에서만 본다 — 대화 이력에 남아 있는 옛 입력창까지 지우면 안 된다
|
|
1865
|
+
const from = Math.max(0, lines.length - 16);
|
|
1866
|
+
let last = -1;
|
|
1867
|
+
for (let i = lines.length - 1; i >= from; i--) {
|
|
1868
|
+
if (/^[─━═]{2,}$/.test(lines[i].text.trim())) {
|
|
1869
|
+
last = i;
|
|
1870
|
+
break;
|
|
1871
|
+
}
|
|
1872
|
+
}
|
|
1873
|
+
if (last < 0) {
|
|
1874
|
+
return false;
|
|
1875
|
+
}
|
|
1876
|
+
// 마지막 테두리에서 위로 최대 4행까지만 같은 입력창 블록으로 본다
|
|
1877
|
+
let start = last;
|
|
1878
|
+
for (let i = last - 1; i >= Math.max(from, last - 4); i--) {
|
|
1879
|
+
if (/^[─━═]{2,}$/.test(lines[i].text.trim())) {
|
|
1880
|
+
start = i;
|
|
1881
|
+
}
|
|
1882
|
+
}
|
|
1883
|
+
const row = start + 1;
|
|
1884
|
+
this.diag(`${new Date().toISOString()} clear input area row=${row}/${lines.length}`);
|
|
1885
|
+
try {
|
|
1886
|
+
xterm.write(`7[${row};1H[J8`);
|
|
1887
|
+
}
|
|
1888
|
+
catch {
|
|
1889
|
+
return false;
|
|
1890
|
+
}
|
|
1891
|
+
return true;
|
|
1892
|
+
}
|
|
1893
|
+
// ---------- 화면 깨짐 자동 감지 ----------
|
|
1894
|
+
/**
|
|
1895
|
+
* 출력이 잠잠해지면 화면이 온전한지 본다.
|
|
1896
|
+
*
|
|
1897
|
+
* TUI 는 한 프레임을 여러 번의 write 로 그리므로, 그리는 도중에 읽으면 멀쩡한 화면도
|
|
1898
|
+
* 깨져 보인다. 그래서 마지막 출력에서 `SCREEN_CHECK_DEBOUNCE_MS` 만큼 조용해진 뒤에만 본다.
|
|
1899
|
+
*/
|
|
1900
|
+
scheduleScreenCheck(pane) {
|
|
1901
|
+
// 감지·채증은 복구와 분리한다 — `autoRepairScreen` 이 꺼져 있어도 관측은 계속한다.
|
|
1902
|
+
// (2026-09-02: 이 게이트가 채증까지 막아 실사용 재현이 통째로 증거 없이 지나갔다)
|
|
1903
|
+
if (!this.screenObservingEnabled()) {
|
|
1904
|
+
return;
|
|
1905
|
+
}
|
|
1906
|
+
const prev = this.screenCheckTimers.get(pane);
|
|
1907
|
+
if (prev) {
|
|
1908
|
+
clearTimeout(prev);
|
|
1909
|
+
}
|
|
1910
|
+
this.screenCheckTimers.set(pane, setTimeout(() => {
|
|
1911
|
+
this.screenCheckTimers.delete(pane);
|
|
1912
|
+
this.checkScreen(pane);
|
|
1913
|
+
}, SCREEN_CHECK_DEBOUNCE_MS));
|
|
1914
|
+
}
|
|
1915
|
+
/** 감지·채증이 켜져 있나 — 복구(`autoRepairScreen`)와는 별개다 */
|
|
1916
|
+
screenObservingEnabled() {
|
|
1917
|
+
const cfg = this.config.store.agentDeck;
|
|
1918
|
+
return cfg.screenWatch !== false || !!cfg.autoRepairScreen;
|
|
1919
|
+
}
|
|
1920
|
+
/** 화면을 판정해 채증하고, 복구가 켜져 있으면 조용히 고친다 */
|
|
1921
|
+
checkScreen(pane) {
|
|
1922
|
+
var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m, _o, _p;
|
|
1923
|
+
const cfg = this.config.store.agentDeck;
|
|
1924
|
+
const read = this.readScreen(pane);
|
|
1925
|
+
if (!read) {
|
|
1926
|
+
return;
|
|
1927
|
+
}
|
|
1928
|
+
const verdict = (0,_screen__WEBPACK_IMPORTED_MODULE_10__.judgeScreen)(read.lines, read.cols);
|
|
1929
|
+
// ---- 관측 (복구와 무관하게 항상) ----
|
|
1930
|
+
// 상태가 바뀔 때만 남긴다 — 2초마다 도는 그물이라 매번 찍으면 파일이 같은 줄로 찬다.
|
|
1931
|
+
const prevObs = this.screenObs.get(pane);
|
|
1932
|
+
const reason = verdict.reasons.join(' / ');
|
|
1933
|
+
// 같은 깨짐이 몇 번 연속으로 보였나 — TUI 가 한 프레임을 여러 write 로 그리는 도중을
|
|
1934
|
+
// 잡으면 멀쩡한 화면도 한 번은 깨져 보인다. 그 중간 프레임에 손대지 않으려고 센다.
|
|
1935
|
+
const streak = verdict.broken ? (((prevObs === null || prevObs === void 0 ? void 0 : prevObs.broken) ? ((_a = prevObs.streak) !== null && _a !== void 0 ? _a : 0) : 0) + 1) : 0;
|
|
1936
|
+
const changed = !prevObs || prevObs.broken !== verdict.broken || prevObs.reason !== reason;
|
|
1937
|
+
this.screenObs.set(pane, { broken: verdict.broken, reason, streak });
|
|
1938
|
+
if (changed) {
|
|
1939
|
+
if (verdict.broken) {
|
|
1940
|
+
this.snapshot(pane, `detect 깨짐 ${reason}`);
|
|
1941
|
+
}
|
|
1942
|
+
else if (prevObs === null || prevObs === void 0 ? void 0 : prevObs.broken) {
|
|
1943
|
+
// 우리가 아무것도 안 했는데 스스로 돌아왔다 — 트리거를 가르는 중요한 단서다
|
|
1944
|
+
this.snapshot(pane, 'detect 스스로 복구됨');
|
|
1945
|
+
}
|
|
1946
|
+
}
|
|
1947
|
+
if (!verdict.broken) {
|
|
1948
|
+
return;
|
|
1949
|
+
}
|
|
1950
|
+
if (!cfg.autoRepairScreen) {
|
|
1951
|
+
return;
|
|
1952
|
+
}
|
|
1953
|
+
// 한 번 스친 중간 프레임은 건드리지 않는다 — 2초 그물이므로 기본값 2 는 "2초 넘게
|
|
1954
|
+
// 깨진 채로 있다" 는 뜻이다. 예전에 자동복구가 타이핑 중에 튀어나와 체감을 망친 것이
|
|
1955
|
+
// 이 조건이 없어서였다 (2026-09-02 항목 27).
|
|
1956
|
+
if (streak < ((_b = cfg.autoRepairMinStreak) !== null && _b !== void 0 ? _b : 2)) {
|
|
1957
|
+
return;
|
|
1958
|
+
}
|
|
1356
1959
|
const anyPane = pane;
|
|
1960
|
+
const now = Date.now();
|
|
1961
|
+
const last = (_c = this.autoRepairAt.get(pane)) !== null && _c !== void 0 ? _c : 0;
|
|
1962
|
+
const count = (_d = this.autoRepairCount.get(pane)) !== null && _d !== void 0 ? _d : 0;
|
|
1963
|
+
const max = (_e = cfg.autoRepairMaxPerTab) !== null && _e !== void 0 ? _e : 5;
|
|
1964
|
+
const cooldown = (_f = cfg.autoRepairCooldownMs) !== null && _f !== void 0 ? _f : 4000;
|
|
1965
|
+
// 쿨다운/한도에 걸려 아무것도 하지 않을 때는 로그도 남기지 않는다 —
|
|
1966
|
+
// 주기 검사가 2초마다 돌므로 판정이 오탐이면 진단 파일이 같은 줄로 가득 찬다
|
|
1967
|
+
if (now - last < cooldown) {
|
|
1968
|
+
return;
|
|
1969
|
+
}
|
|
1970
|
+
if (max > 0 && count >= max) {
|
|
1971
|
+
this.diagOnce(`auto-repair-cap`, `screen auto-repair 한도 도달 (${count}/${max}) — 더 고치지 않는다.`
|
|
1972
|
+
+ ` 사이드바 ↻ 로 직접 복구할 것. 마지막 판정: ${verdict.reasons.join(' / ')}`);
|
|
1973
|
+
return;
|
|
1974
|
+
}
|
|
1975
|
+
this.diag(`${new Date().toISOString()} screen BROKEN cols=${read.cols}`
|
|
1976
|
+
+ ` sent=${(_g = anyPane.size) === null || _g === void 0 ? void 0 : _g.columns}x${(_h = anyPane.size) === null || _h === void 0 ? void 0 : _h.rows} ${verdict.reasons.join(' / ')}`);
|
|
1977
|
+
// 고치기 **전에** 화면 원문을 남긴다 — 복구가 덮어쓰면 증거가 사라진다
|
|
1978
|
+
this.snapshot(pane, `auto-before ${verdict.reasons.join(' / ')}`);
|
|
1979
|
+
this.autoRepairAt.set(pane, now);
|
|
1980
|
+
this.autoRepairCount.set(pane, count + 1);
|
|
1981
|
+
this.diag(`${new Date().toISOString()} screen auto-repair #${count + 1}`);
|
|
1982
|
+
// 스크롤백을 지우는 hard 복구는 하지 않는다 — 자동으로 도는 경로에서 오탐 한 번에
|
|
1983
|
+
// 지난 대화가 날아가면 손해가 크다. 크기 재동기 + pty 흔들기 + Ctrl+L 까지만 한다.
|
|
1984
|
+
// 실측(2026-09-01): 세션이 열린 직후 유실됐던 resize 도 이 흔들기로는 pty 에 도달한다.
|
|
1357
1985
|
const frontend = anyPane.frontend;
|
|
1358
|
-
|
|
1359
|
-
|
|
1360
|
-
|
|
1361
|
-
|
|
1362
|
-
|
|
1363
|
-
|
|
1364
|
-
|
|
1365
|
-
|
|
1366
|
-
|
|
1986
|
+
// 크기를 건드리는 건 **실제로 어긋났을 때만.** 이번에 실사용에서 잡힌 유형은
|
|
1987
|
+
// xterm 도 pty 도 277 로 맞는데 화면만 깨져 있었다 — 거기에 흔들기를 때리면
|
|
1988
|
+
// 고치지도 못하면서 화면만 흔든다(`auto-after 아직 깨짐` 2연속이 그 결과다).
|
|
1989
|
+
const xcols = (_j = frontend === null || frontend === void 0 ? void 0 : frontend.xterm) === null || _j === void 0 ? void 0 : _j.cols;
|
|
1990
|
+
const xrows = (_k = frontend === null || frontend === void 0 ? void 0 : frontend.xterm) === null || _k === void 0 ? void 0 : _k.rows;
|
|
1991
|
+
if (xcols > 1 && (((_l = anyPane.size) === null || _l === void 0 ? void 0 : _l.columns) !== xcols || ((_m = anyPane.size) === null || _m === void 0 ? void 0 : _m.rows) !== xrows)) {
|
|
1992
|
+
try {
|
|
1993
|
+
(_p = (_o = frontend === null || frontend === void 0 ? void 0 : frontend.fitAddon) === null || _o === void 0 ? void 0 : _o.fit) === null || _p === void 0 ? void 0 : _p.call(_o);
|
|
1994
|
+
}
|
|
1995
|
+
catch {
|
|
1996
|
+
// 붙는 중이다 — 아래 단계만이라도 태운다
|
|
1367
1997
|
}
|
|
1998
|
+
this.syncPtySize(pane, frontend);
|
|
1999
|
+
this.nudgePtyRedraw(pane);
|
|
2000
|
+
}
|
|
2001
|
+
// 잔상을 **먼저** 지우고 그 다음에 앱에게 다시 그리라고 한다.
|
|
2002
|
+
// 앱은 자기가 쓰는 자리만 덮으므로, 지우지 않으면 옛 줄이 `❯` 자리를 계속 차지한다.
|
|
2003
|
+
this.clearInputArea(pane, read);
|
|
2004
|
+
if (cfg.repairSendRedrawKey !== false) {
|
|
2005
|
+
setTimeout(() => this.sendRedrawKey(pane), REPAIR_REDRAW_DELAY_MS);
|
|
2006
|
+
}
|
|
2007
|
+
// 복구가 실제로 먹혔는지 같은 파일에 이어 남긴다 — 앱이 다시 그릴 시간을 준다
|
|
2008
|
+
setTimeout(() => {
|
|
2009
|
+
const after = this.readScreen(pane);
|
|
2010
|
+
const still = after ? (0,_screen__WEBPACK_IMPORTED_MODULE_10__.judgeScreen)(after.lines, after.cols) : null;
|
|
2011
|
+
this.snapshot(pane, `auto-after ${still ? (still.broken ? '아직 깨짐: ' + still.reasons.join(' / ') : '복구됨') : '읽을 수 없음'}`);
|
|
2012
|
+
}, SNAP_AFTER_MS);
|
|
2013
|
+
}
|
|
2014
|
+
/** 활성 탭의 pane 들을 한 번씩 본다 (주기 그물) */
|
|
2015
|
+
sweepScreens() {
|
|
2016
|
+
if (!this.enabled || !this.screenObservingEnabled()) {
|
|
2017
|
+
return;
|
|
2018
|
+
}
|
|
2019
|
+
const tab = this.app.activeTab;
|
|
2020
|
+
if (!tab) {
|
|
2021
|
+
return;
|
|
2022
|
+
}
|
|
2023
|
+
const anyTab = tab;
|
|
2024
|
+
const panes = typeof anyTab.getAllTabs === 'function'
|
|
2025
|
+
? anyTab.getAllTabs()
|
|
2026
|
+
: [tab];
|
|
2027
|
+
for (const pane of panes) {
|
|
2028
|
+
this.checkScreen(pane);
|
|
2029
|
+
}
|
|
2030
|
+
}
|
|
2031
|
+
/** 판정에 넣을 화면 줄과 폭 — 숨겨진 탭이나 아직 안 붙은 프론트엔드는 건너뛴다 */
|
|
2032
|
+
readScreen(pane) {
|
|
2033
|
+
var _a, _b, _c, _d;
|
|
2034
|
+
const anyPane = pane;
|
|
2035
|
+
const xterm = (_a = anyPane.frontend) === null || _a === void 0 ? void 0 : _a.xterm;
|
|
2036
|
+
const buf = (_b = xterm === null || xterm === void 0 ? void 0 : xterm.buffer) === null || _b === void 0 ? void 0 : _b.active;
|
|
2037
|
+
if (!buf || typeof buf.getLine !== 'function' || !(xterm.cols > 1)) {
|
|
2038
|
+
return null;
|
|
2039
|
+
}
|
|
2040
|
+
// 숨겨진 탭은 컨테이너 폭이 0 이라 xterm 이 1열로 줄어 있다 — 판정 대상이 아니다
|
|
2041
|
+
const host = (_c = xterm.element) === null || _c === void 0 ? void 0 : _c.parentElement;
|
|
2042
|
+
if (!(host === null || host === void 0 ? void 0 : host.clientWidth)) {
|
|
2043
|
+
return null;
|
|
2044
|
+
}
|
|
2045
|
+
const lines = [];
|
|
2046
|
+
for (let y = buf.baseY; y <= buf.baseY + xterm.rows - 1; y++) {
|
|
2047
|
+
const line = buf.getLine(y);
|
|
2048
|
+
lines.push({
|
|
2049
|
+
text: (_d = line === null || line === void 0 ? void 0 : line.translateToString(false)) !== null && _d !== void 0 ? _d : '',
|
|
2050
|
+
wrapped: !!(line === null || line === void 0 ? void 0 : line.isWrapped),
|
|
2051
|
+
});
|
|
2052
|
+
}
|
|
2053
|
+
return { lines, cols: xterm.cols };
|
|
2054
|
+
}
|
|
2055
|
+
/**
|
|
2056
|
+
* 화면 원문을 통째로 남긴다 — 깨짐 채증용.
|
|
2057
|
+
*
|
|
2058
|
+
* 판정 이유만 남기면 다음에 또 "왜 그렇게 됐는지" 를 추측하게 된다. 행 번호·wrap 플래그·
|
|
2059
|
+
* 길이까지 그대로 적어 두면 `❯` 가 어느 행에 있었는지, 테두리가 몇 칸이었는지가 확정된다.
|
|
2060
|
+
* before/after 를 짝으로 남기므로 복구가 실제로 먹혔는지도 이 파일에서 바로 갈린다.
|
|
2061
|
+
*/
|
|
2062
|
+
snapshot(pane, tag) {
|
|
2063
|
+
var _a, _b, _c, _d;
|
|
2064
|
+
const read = this.readScreen(pane);
|
|
2065
|
+
if (!read) {
|
|
1368
2066
|
return;
|
|
1369
2067
|
}
|
|
1370
|
-
|
|
1371
|
-
|
|
2068
|
+
const anyPane = pane;
|
|
2069
|
+
const xterm = (_a = anyPane.frontend) === null || _a === void 0 ? void 0 : _a.xterm;
|
|
2070
|
+
const head = `\n===== ${new Date().toISOString()} ${tag}`
|
|
2071
|
+
+ ` cols=${read.cols} rows=${xterm === null || xterm === void 0 ? void 0 : xterm.rows} sent=${(_b = anyPane.size) === null || _b === void 0 ? void 0 : _b.columns}x${(_c = anyPane.size) === null || _c === void 0 ? void 0 : _c.rows}`
|
|
2072
|
+
+ ` title=${JSON.stringify((_d = pane.title) !== null && _d !== void 0 ? _d : '')} =====`;
|
|
2073
|
+
const body = read.lines
|
|
2074
|
+
.map((l, i) => ({ i, t: l.text.replace(/\s+$/, ''), w: l.wrapped }))
|
|
2075
|
+
.filter(l => l.t)
|
|
2076
|
+
.map(l => `${String(l.i).padStart(3)} w=${l.w ? 1 : 0} len=${l.t.length} ${JSON.stringify(l.t)}`);
|
|
1372
2077
|
try {
|
|
1373
|
-
|
|
1374
|
-
|
|
1375
|
-
|
|
1376
|
-
//
|
|
1377
|
-
|
|
1378
|
-
|
|
2078
|
+
fs__WEBPACK_IMPORTED_MODULE_3__.appendFileSync(SNAP_PATH, [head, ...body, ''].join('\n'));
|
|
2079
|
+
}
|
|
2080
|
+
catch {
|
|
2081
|
+
// 진단일 뿐이라 실패해도 동작에는 영향이 없다
|
|
2082
|
+
}
|
|
2083
|
+
}
|
|
2084
|
+
// ---------- 세션 감시 ----------
|
|
2085
|
+
/**
|
|
2086
|
+
* 탭(그리고 그 안의 모든 pane)의 PTY 출력을 구독한다.
|
|
2087
|
+
*
|
|
2088
|
+
* TerminalDecorator 는 프론트엔드 첫 리사이즈 시점에만 붙어서 복원된 탭 등을
|
|
2089
|
+
* 놓치는 경우가 있다. 이쪽은 탭이 목록에 들어오는 즉시 붙으므로 그 빈틈을 메운다.
|
|
2090
|
+
* 같은 출력을 decorator 와 둘 다 먹어도 상태 설정이 멱등이라 결과는 같다.
|
|
2091
|
+
*/
|
|
2092
|
+
watchTab(root) {
|
|
2093
|
+
var _a;
|
|
2094
|
+
const anyRoot = root;
|
|
2095
|
+
if (!this.watched.has(root)) {
|
|
2096
|
+
this.watched.add(root);
|
|
2097
|
+
// 제목은 래퍼 탭이 자식 것을 위임받으므로 여기서 받는 게 가장 확실하다
|
|
2098
|
+
if (anyRoot.titleChange$) {
|
|
2099
|
+
anyRoot.titleChange$.subscribe((title) => {
|
|
2100
|
+
(0,_detect__WEBPACK_IMPORTED_MODULE_7__.applyTitle)(this.status, root, title, this.config.store.agentDeck.autoDetect);
|
|
2101
|
+
});
|
|
2102
|
+
}
|
|
2103
|
+
if (anyRoot.title) {
|
|
2104
|
+
(0,_detect__WEBPACK_IMPORTED_MODULE_7__.applyTitle)(this.status, root, anyRoot.title, this.config.store.agentDeck.autoDetect);
|
|
2105
|
+
}
|
|
2106
|
+
// 나중에 분할로 추가되는 pane 도 따라간다
|
|
2107
|
+
if (anyRoot.tabAdded$) {
|
|
2108
|
+
anyRoot.tabAdded$.subscribe((pane) => this.watchPane(root, pane));
|
|
2109
|
+
}
|
|
2110
|
+
}
|
|
2111
|
+
// pane 은 래퍼보다 늦게 붙는 경우가 있으므로 호출될 때마다 다시 훑는다
|
|
2112
|
+
// (래퍼만 보고 한 번에 끝내면 목록에 먼저 들어온 빈 SplitTab 을 잡고 끝나버린다)
|
|
2113
|
+
const panes = typeof anyRoot.getAllTabs === 'function'
|
|
2114
|
+
? anyRoot.getAllTabs()
|
|
2115
|
+
: [root];
|
|
2116
|
+
this.diag(`${new Date().toISOString()} watchTab panes=${panes.length}`);
|
|
2117
|
+
for (const pane of panes) {
|
|
2118
|
+
this.watchPane(root, pane);
|
|
2119
|
+
}
|
|
2120
|
+
// 래퍼(SplitTabComponent)가 자식보다 먼저 목록에 들어오는 경우가 있다. 그때 여기서
|
|
2121
|
+
// panes=0 을 보고 물러나면, 자식이 붙어도 다시 훑을 계기가 없어 그 탭은 영영
|
|
2122
|
+
// 감시 밖에 남는다 — 상태 감지·Enter 라벨·화면 검사가 전부 안 돈다
|
|
2123
|
+
// (2026-09-01 실측: 기동 직후 첫 탭에서 `watchTab panes=0` 만 두 번 찍히고
|
|
2124
|
+
// `session bind` 가 한 번도 없었다). 그래서 비어 있으면 잠시 뒤 다시 본다.
|
|
2125
|
+
if (!panes.length) {
|
|
2126
|
+
const tries = ((_a = this.emptyTabRetries.get(root)) !== null && _a !== void 0 ? _a : 0) + 1;
|
|
2127
|
+
this.emptyTabRetries.set(root, tries);
|
|
2128
|
+
if (tries <= EMPTY_TAB_RETRY_MAX) {
|
|
2129
|
+
setTimeout(() => {
|
|
2130
|
+
if (this.app.tabs.includes(root)) {
|
|
2131
|
+
this.watchTab(root);
|
|
2132
|
+
}
|
|
2133
|
+
}, EMPTY_TAB_RETRY_MS);
|
|
2134
|
+
}
|
|
2135
|
+
}
|
|
2136
|
+
}
|
|
2137
|
+
watchPane(root, pane) {
|
|
2138
|
+
var _a;
|
|
2139
|
+
if (this.watchedPanes.has(pane)) {
|
|
2140
|
+
return;
|
|
2141
|
+
}
|
|
2142
|
+
this.watchedPanes.add(pane);
|
|
2143
|
+
this.diag(`${new Date().toISOString()} watchPane ${(_a = pane.constructor) === null || _a === void 0 ? void 0 : _a.name}`);
|
|
2144
|
+
const anyPane = pane;
|
|
2145
|
+
const bind = (session) => {
|
|
2146
|
+
var _a, _b, _c, _d, _e, _f;
|
|
2147
|
+
if (!session || !session.output$) {
|
|
2148
|
+
return;
|
|
2149
|
+
}
|
|
2150
|
+
// pty 가 어떤 폭으로 태어났는지 남긴다. Tabby 는 프론트엔드의 **첫** resize$ 값으로
|
|
2151
|
+
// 세션을 스폰하므로(tabby-local: initializeSession(this.size...) -> spawn cols),
|
|
2152
|
+
// 그 스냅샷이 우리 레이아웃보다 이르면 pty 만 옛 폭으로 굳는다 — 그 순간이 여기 찍힌다.
|
|
2153
|
+
this.diag(`${new Date().toISOString()} session bind`
|
|
2154
|
+
+ ` xterm=${(_b = (_a = anyPane.frontend) === null || _a === void 0 ? void 0 : _a.xterm) === null || _b === void 0 ? void 0 : _b.cols}x${(_d = (_c = anyPane.frontend) === null || _c === void 0 ? void 0 : _c.xterm) === null || _d === void 0 ? void 0 : _d.rows}`
|
|
2155
|
+
+ ` paneSize=${(_e = anyPane.size) === null || _e === void 0 ? void 0 : _e.columns}x${(_f = anyPane.size) === null || _f === void 0 ? void 0 : _f.rows}`);
|
|
2156
|
+
session.output$.subscribe((data) => {
|
|
2157
|
+
(0,_detect__WEBPACK_IMPORTED_MODULE_7__.applyOutput)(this.status, root, pane, data, this.config.store.agentDeck.autoDetect);
|
|
2158
|
+
// 출력이 멈춘 뒤 화면이 온전한지 본다 (그리는 도중에 읽으면 멀쩡한 화면도 깨져 보인다)
|
|
2159
|
+
this.scheduleScreenCheck(pane);
|
|
2160
|
+
});
|
|
2161
|
+
};
|
|
2162
|
+
bind(anyPane.session);
|
|
2163
|
+
if (anyPane.sessionChanged$) {
|
|
2164
|
+
anyPane.sessionChanged$.subscribe((session) => bind(session));
|
|
2165
|
+
}
|
|
2166
|
+
// 프론트엔드가 붙는 도중에 우리가 폭을 바꿨으면 그 변경은 유실될 수 있다
|
|
2167
|
+
// (Tabby 는 attach() 맨 끝에서야 ResizeObserver 를 건다) — 준비되면 fit 한 번으로 깨워 준다.
|
|
2168
|
+
// 여기서 하는 건 `fitAddon.fit()` 뿐이다. 그 뒤는 순정 경로가 이어받는다
|
|
2169
|
+
// (fit → xterm onResize → Tabby 가 session.resize). 우리가 pty 에 직접 쏘지 않는다.
|
|
2170
|
+
if (anyPane.frontendReady$) {
|
|
2171
|
+
anyPane.frontendReady$.subscribe(() => {
|
|
2172
|
+
this.scheduleRefit();
|
|
2173
|
+
this.announceSizeOnOpen(pane);
|
|
2174
|
+
});
|
|
2175
|
+
}
|
|
2176
|
+
// frontendReady 는 replay 가 없는 맨 Subject 라, 복원된 탭의 프론트엔드가 우리보다
|
|
2177
|
+
// 먼저 붙어버리면 위 구독은 영영 못 받는다. 이벤트와 무관하게 한 번 더 예약한다 —
|
|
2178
|
+
// 같은 크기를 두 번 알려도 SIGWINCH 가 안 나므로 무해하다.
|
|
2179
|
+
this.announceSizeOnOpen(pane);
|
|
2180
|
+
}
|
|
2181
|
+
/**
|
|
2182
|
+
* 탭이 열린 뒤 몇 차례, **지금 xterm 이 쓰는 크기를 pty 에 알려준다.**
|
|
2183
|
+
*
|
|
2184
|
+
* 순정이 유일하게 못 메우는 구멍(새 탭에서 pty 만 좁게 태어남)을 메우는 최소 개입이다.
|
|
2185
|
+
* 자세한 근거와 실측은 `TAB_OPEN_ANNOUNCE_MS` 주석에 있다.
|
|
2186
|
+
*
|
|
2187
|
+
* 하지 않는 것 — xterm 은 건드리지 않고(fit 도 resize 도), 폭을 흔들지 않으며,
|
|
2188
|
+
* 감시 루프도 돌지 않는다. 그 셋이 예전에 화면을 깨뜨린 장본인이다.
|
|
2189
|
+
*/
|
|
2190
|
+
announceSizeOnOpen(pane) {
|
|
2191
|
+
const anyPane = pane;
|
|
2192
|
+
for (const delay of TAB_OPEN_ANNOUNCE_MS) {
|
|
1379
2193
|
setTimeout(() => {
|
|
1380
|
-
var _a
|
|
2194
|
+
var _a;
|
|
2195
|
+
const x = (_a = anyPane.frontend) === null || _a === void 0 ? void 0 : _a.xterm;
|
|
2196
|
+
const session = anyPane.session;
|
|
2197
|
+
if (!(session === null || session === void 0 ? void 0 : session.open) || typeof session.resize !== 'function' || !((x === null || x === void 0 ? void 0 : x.cols) > 1)) {
|
|
2198
|
+
return;
|
|
2199
|
+
}
|
|
2200
|
+
const prev = anyPane.size;
|
|
2201
|
+
if ((prev === null || prev === void 0 ? void 0 : prev.columns) !== x.cols || (prev === null || prev === void 0 ? void 0 : prev.rows) !== x.rows) {
|
|
2202
|
+
this.diag(`${new Date().toISOString()} announce size ${prev === null || prev === void 0 ? void 0 : prev.columns}x${prev === null || prev === void 0 ? void 0 : prev.rows}`
|
|
2203
|
+
+ ` -> ${x.cols}x${x.rows} (+${delay}ms)`);
|
|
2204
|
+
}
|
|
1381
2205
|
try {
|
|
1382
|
-
|
|
1383
|
-
|
|
1384
|
-
if (typeof ((_a = frontend === null || frontend === void 0 ? void 0 : frontend.fitAddon) === null || _a === void 0 ? void 0 : _a.fit) === 'function') {
|
|
1385
|
-
frontend.fitAddon.fit();
|
|
1386
|
-
}
|
|
1387
|
-
else {
|
|
1388
|
-
(_c = (_b = frontend === null || frontend === void 0 ? void 0 : frontend.xterm) === null || _b === void 0 ? void 0 : _b.resize) === null || _c === void 0 ? void 0 : _c.call(_b, cols, rows);
|
|
1389
|
-
}
|
|
1390
|
-
// 되돌릴 값은 fit() 이 실제로 잡은 폭이다. 흔들기 시작 시점의 cols 를
|
|
1391
|
-
// 그대로 pty 에 다시 박으면 그 사이 사이드바 폭이 바뀌었거나 fit 결과가
|
|
1392
|
-
// 달라진 경우 pty(옛 값) > xterm(실제) 로 어긋난 채 고정된다 —
|
|
1393
|
-
// TUI 는 넓은 폭으로 그리는데 화면은 좁아 입력창이 오른쪽으로 밀려 잘린다
|
|
1394
|
-
// (2026-08-28 실측: 새 탭에서 줄바꿈하는 순간 드러남).
|
|
1395
|
-
const fitCols = (_e = (_d = frontend === null || frontend === void 0 ? void 0 : frontend.xterm) === null || _d === void 0 ? void 0 : _d.cols) !== null && _e !== void 0 ? _e : cols;
|
|
1396
|
-
const fitRows = (_g = (_f = frontend === null || frontend === void 0 ? void 0 : frontend.xterm) === null || _f === void 0 ? void 0 : _f.rows) !== null && _g !== void 0 ? _g : rows;
|
|
1397
|
-
if (fitCols !== cols || fitRows !== rows) {
|
|
1398
|
-
this.diag(`${new Date().toISOString()} kick refit ${cols}x${rows} -> ${fitCols}x${fitRows}`);
|
|
1399
|
-
}
|
|
1400
|
-
session.resize(fitCols, fitRows);
|
|
1401
|
-
anyPane.size = { columns: fitCols, rows: fitRows };
|
|
2206
|
+
session.resize(x.cols, x.rows);
|
|
2207
|
+
anyPane.size = { columns: x.cols, rows: x.rows };
|
|
1402
2208
|
}
|
|
1403
2209
|
catch {
|
|
1404
|
-
// 세션이 닫혔다
|
|
2210
|
+
// 세션이 막 닫혔다
|
|
1405
2211
|
}
|
|
1406
|
-
},
|
|
1407
|
-
}
|
|
1408
|
-
catch {
|
|
1409
|
-
// 세션이 막 닫혔다
|
|
2212
|
+
}, delay);
|
|
1410
2213
|
}
|
|
1411
2214
|
}
|
|
2215
|
+
// 흔들기 장치는 전부 폐지했다 (2026-09-02).
|
|
2216
|
+
//
|
|
2217
|
+
// 여기에 있던 것들 — `scheduleWindowKick`(기동 시 창 복원→최대화), `scheduleRedrawKick`,
|
|
2218
|
+
// `kickPtySize`(탭당 한 번 cols-1 ↔ cols) — 는 모두 "순정이 크기를 못 맞춘다" 는 전제에서
|
|
2219
|
+
// 나온 우회책이었다. 전제가 틀렸다: Tabby 는 xterm `ResizeObserver` → `fitAddon.fit()` →
|
|
2220
|
+
// `session.resize()` 로 스스로 맞춘다. 우리가 폭만 정해 주면 된다.
|
|
2221
|
+
// 이 흔들기들은 순정 수렴과 경쟁하며 프레임을 반쯤 그린 상태를 남겨 화면을 깨뜨렸다.
|
|
2222
|
+
// 수동 복구(`repairPane`)는 여전히 흔든다 — 거기는 사용자가 명시로 요청한 경로다.
|
|
1412
2223
|
/**
|
|
1413
2224
|
* Ctrl+V 를 **핫키 테이블에서 걷어낸다** (실제 처리는 claimCtrlVKey 가 한다).
|
|
1414
2225
|
*
|
|
@@ -1929,15 +2740,53 @@ let AgentDeckService = class AgentDeckService {
|
|
|
1929
2740
|
this.sidebar.style.display = this.enabled ? '' : 'none';
|
|
1930
2741
|
}
|
|
1931
2742
|
if (!this.enabled && this.mainEl) {
|
|
1932
|
-
|
|
1933
|
-
|
|
1934
|
-
|
|
1935
|
-
|
|
2743
|
+
for (const prop of ['flex', 'width', 'max-width', 'height',
|
|
2744
|
+
'margin-left', 'margin-right', 'margin-top', 'margin-bottom']) {
|
|
2745
|
+
this.mainEl.style.removeProperty(prop);
|
|
2746
|
+
}
|
|
1936
2747
|
// 폭을 되돌렸으니 메모도 무효화하고 터미널에 새 폭을 알린다
|
|
1937
2748
|
this.lastTermW = -1;
|
|
1938
2749
|
this.lastSidebarW = -1;
|
|
2750
|
+
this.lastDock = null;
|
|
1939
2751
|
this.scheduleRefit();
|
|
1940
2752
|
}
|
|
2753
|
+
this.syncBackgroundClip();
|
|
2754
|
+
}
|
|
2755
|
+
/**
|
|
2756
|
+
* tabby-background 의 이미지 레이어를 터미널 영역(`.content.main`)에 맞춰 자른다.
|
|
2757
|
+
*
|
|
2758
|
+
* 그 레이어는 `position: fixed` 라 viewport 좌표를 쓰므로 `getBoundingClientRect()` 값을
|
|
2759
|
+
* 그대로 CSS 변수로 내리면 된다 (styles.scss 가 `!important` 로 덮어쓴다).
|
|
2760
|
+
* relayout 직후에는 인라인 폭이 아직 레이아웃에 반영되지 않았을 수 있어 rAF 한 번 뒤에 잰다.
|
|
2761
|
+
* 꺼져 있으면(또는 agentdeck 비활성) 클래스와 변수를 걷어 순정(창 전체)으로 되돌린다.
|
|
2762
|
+
*/
|
|
2763
|
+
syncBackgroundClip() {
|
|
2764
|
+
const root = document.documentElement;
|
|
2765
|
+
const clear = () => {
|
|
2766
|
+
document.body.classList.remove(BG_CLIP_CLASS);
|
|
2767
|
+
for (const v of ['--ad-bg-left', '--ad-bg-top', '--ad-bg-width', '--ad-bg-height']) {
|
|
2768
|
+
root.style.removeProperty(v);
|
|
2769
|
+
}
|
|
2770
|
+
};
|
|
2771
|
+
if (!this.enabled || !this.config.store.agentDeck.clipBackgroundToTerminal || !this.mainEl) {
|
|
2772
|
+
clear();
|
|
2773
|
+
return;
|
|
2774
|
+
}
|
|
2775
|
+
requestAnimationFrame(() => {
|
|
2776
|
+
if (!this.enabled || !this.mainEl) {
|
|
2777
|
+
clear();
|
|
2778
|
+
return;
|
|
2779
|
+
}
|
|
2780
|
+
const r = this.mainEl.getBoundingClientRect();
|
|
2781
|
+
if (r.width <= 0 || r.height <= 0) {
|
|
2782
|
+
return;
|
|
2783
|
+
}
|
|
2784
|
+
root.style.setProperty('--ad-bg-left', Math.round(r.left) + 'px');
|
|
2785
|
+
root.style.setProperty('--ad-bg-top', Math.round(r.top) + 'px');
|
|
2786
|
+
root.style.setProperty('--ad-bg-width', Math.round(r.width) + 'px');
|
|
2787
|
+
root.style.setProperty('--ad-bg-height', Math.round(r.height) + 'px');
|
|
2788
|
+
document.body.classList.add(BG_CLIP_CLASS);
|
|
2789
|
+
});
|
|
1941
2790
|
}
|
|
1942
2791
|
/**
|
|
1943
2792
|
* 터미널/사이드바 배경에 알파를 넣어 tabby-background 의 배경 이미지가 비치게 한다.
|
|
@@ -1979,8 +2828,46 @@ let AgentDeckService = class AgentDeckService {
|
|
|
1979
2828
|
this.config.store.terminal.background = wantMode;
|
|
1980
2829
|
this.config.save();
|
|
1981
2830
|
}
|
|
1982
|
-
/**
|
|
2831
|
+
/** 지금 사이드바가 붙어 있는 방향 (설정값이 망가져 있으면 오른쪽으로 본다) */
|
|
2832
|
+
get dockSide() {
|
|
2833
|
+
const v = this.config.store.agentDeck.sidebarDock;
|
|
2834
|
+
return (0,_dock__WEBPACK_IMPORTED_MODULE_11__.isDockSide)(v) ? v : 'right';
|
|
2835
|
+
}
|
|
2836
|
+
/**
|
|
2837
|
+
* 사이드바가 차지할 크기를 px 로 정한다 — 좌/우면 폭, 상/하면 높이.
|
|
2838
|
+
*
|
|
2839
|
+
* 사용자가 경계선을 한 번이라도 끌었으면 그 값(`sidebarWidth`/`sidebarHeight`)이 진실이다.
|
|
2840
|
+
* 아직 안 끌었다면(폭이 0) 예전처럼 화면비로 터미널 폭을 역산해 남는 만큼을 사이드바가 갖는다.
|
|
2841
|
+
*/
|
|
2842
|
+
sidebarExtent(availW, availH, winH) {
|
|
2843
|
+
const cfg = this.config.store.agentDeck;
|
|
2844
|
+
if ((0,_dock__WEBPACK_IMPORTED_MODULE_11__.isHorizontalDock)(this.dockSide)) {
|
|
2845
|
+
const minSidebar = cfg.sidebarMin || 200;
|
|
2846
|
+
const maxSidebar = Math.max(cfg.sidebarMax || 560, minSidebar);
|
|
2847
|
+
let want = cfg.sidebarWidth || 0;
|
|
2848
|
+
if (!want) {
|
|
2849
|
+
// 화면비를 지킨다는 가정에서 터미널 폭을 높이로 역산하고 남는 폭을 사이드바에 준다
|
|
2850
|
+
const ratio = (cfg.aspectW || 4) / (cfg.aspectH || 3);
|
|
2851
|
+
let termW = Math.round(availH * ratio);
|
|
2852
|
+
termW = Math.min(termW, availW - minSidebar);
|
|
2853
|
+
termW = Math.max(termW, MIN_TERM_W);
|
|
2854
|
+
want = availW - termW;
|
|
2855
|
+
}
|
|
2856
|
+
// 손으로 끈 값은 설정의 min/max 를 넘어설 수 있다 — 마우스가 우선이므로
|
|
2857
|
+
// 목록이 읽히는 최소 폭과 터미널이 남을 최대 폭까지만 막는다
|
|
2858
|
+
const hardMin = cfg.sidebarWidth ? MIN_SIDEBAR_W : minSidebar;
|
|
2859
|
+
const hardMax = Math.max(availW - MIN_TERM_W, hardMin);
|
|
2860
|
+
const softMax = cfg.sidebarWidth ? Math.max(maxSidebar, cfg.sidebarWidth) : maxSidebar;
|
|
2861
|
+
return Math.min(Math.max(want, hardMin), softMax, hardMax);
|
|
2862
|
+
}
|
|
2863
|
+
// 세로 도킹은 창 높이를 기준으로 잡는다 — 터미널 높이(availH)는 사이드바를 뺀 값이라
|
|
2864
|
+
// 그걸로 클램프하면 잴 때마다 값이 줄어드는 되먹임이 생긴다
|
|
2865
|
+
const want = cfg.sidebarHeight || 200;
|
|
2866
|
+
return Math.min(Math.max(want, MIN_SIDEBAR_H), Math.max(winH - MIN_TERM_H, MIN_SIDEBAR_H));
|
|
2867
|
+
}
|
|
2868
|
+
/** 사이드바를 창 한쪽에 붙이고, 남는 자리를 `.content.main` 이 갖게 한다 */
|
|
1983
2869
|
relayout() {
|
|
2870
|
+
var _a;
|
|
1984
2871
|
if (!this.enabled || !this.windowEl) {
|
|
1985
2872
|
this.diagOnce('relayout-skip-enabled', `relayout skipped enabled=${this.enabled} windowEl=${!!this.windowEl}`);
|
|
1986
2873
|
return;
|
|
@@ -1993,53 +2880,114 @@ let AgentDeckService = class AgentDeckService {
|
|
|
1993
2880
|
this.diagOnce('relayout-skip-mainel', 'relayout skipped mainEl=null');
|
|
1994
2881
|
return;
|
|
1995
2882
|
}
|
|
1996
|
-
const cfg = this.config.store.agentDeck;
|
|
1997
2883
|
const availW = this.windowEl.clientWidth;
|
|
2884
|
+
// 화면비로 폭을 역산할 때 쓰는 높이는 터미널이 실제로 쓰는 높이여야 한다.
|
|
2885
|
+
// 창 높이를 쓰면 탭바 등을 뺀 만큼 폭이 과대 계산돼 사이드바가 좁아진다
|
|
1998
2886
|
const availH = this.mainEl.clientHeight || this.windowEl.clientHeight;
|
|
1999
2887
|
if (!availW || !availH) {
|
|
2000
2888
|
return;
|
|
2001
2889
|
}
|
|
2002
|
-
const
|
|
2003
|
-
const
|
|
2004
|
-
|
|
2005
|
-
//
|
|
2006
|
-
let termW =
|
|
2007
|
-
|
|
2008
|
-
|
|
2009
|
-
//
|
|
2010
|
-
|
|
2011
|
-
//
|
|
2012
|
-
//
|
|
2013
|
-
|
|
2890
|
+
const side = this.dockSide;
|
|
2891
|
+
const horizontal = (0,_dock__WEBPACK_IMPORTED_MODULE_11__.isHorizontalDock)(side);
|
|
2892
|
+
let extent = Math.round(this.sidebarExtent(availW, availH, this.windowEl.clientHeight));
|
|
2893
|
+
// 터미널이 갖는 폭 — 가로 도킹일 때만 의미가 있다 (세로 도킹이면 폭 전체를 쓴다)
|
|
2894
|
+
let termW = horizontal ? availW - extent : availW;
|
|
2895
|
+
// 터미널 폭을 문자 셀의 정수배로 깎는다.
|
|
2896
|
+
//
|
|
2897
|
+
// 셀 폭으로 나누어떨어지지 않으면 오른쪽에 셀 하나가 안 되는 자투리가 남는데,
|
|
2898
|
+
// fit() 은 그 자투리를 두고 창이 몇 px 만 흔들려도 열 수가 오르내린다. 열 수가 바뀔 때마다
|
|
2899
|
+
// pty 에 새 크기가 나가고 TUI 는 전체를 다시 그린다 — 화면이 깜빡이고 어긋날 틈이 생긴다.
|
|
2900
|
+
// 정수배로 맞춰 두면 자투리가 사라져 열 수가 흔들리지 않는다. 깎아 낸 픽셀은 사이드바가 갖는다.
|
|
2901
|
+
// 한때 이 자리에서 터미널 폭을 문자 셀의 정수배로 깎았다. 자투리 픽셀을 없애려던 건데
|
|
2902
|
+
// 오히려 화면이 계속 밀리는 원인이 됐다 — 셀 폭은 폰트가 자리를 잡는 동안 조금씩 다르게
|
|
2903
|
+
// 잡히고(2026-09-01 실측: 같은 창에서 termW 가 1977 -> 1970 -> 1974 로 진동),
|
|
2904
|
+
// 폭이 바뀔 때마다 pty 를 다시 맞추고 TUI 를 다시 그리게 만들었다.
|
|
2905
|
+
// 폭은 사용자가 정한 사이드바 크기에서 곧장 나오는 값 하나로 고정하는 편이 안정적이다.
|
|
2906
|
+
// 남는 자투리는 어차피 배경색이라 눈에 띄지 않는다.
|
|
2907
|
+
extent = Math.round(extent);
|
|
2908
|
+
termW = Math.round(termW);
|
|
2014
2909
|
// 값이 그대로면 DOM 을 건드리지 않는다 — relayout 은 탭/상태가 바뀔 때마다 불리므로
|
|
2015
2910
|
// 매번 스타일을 다시 쓰면 xterm 이 불필요하게 재측정한다
|
|
2016
|
-
if (termW === this.lastTermW &&
|
|
2911
|
+
if (termW === this.lastTermW && extent === this.lastSidebarW && side === this.lastDock) {
|
|
2017
2912
|
return;
|
|
2018
2913
|
}
|
|
2019
2914
|
const prevTermW = this.lastTermW;
|
|
2020
2915
|
this.lastTermW = termW;
|
|
2021
|
-
this.lastSidebarW =
|
|
2022
|
-
|
|
2023
|
-
//
|
|
2024
|
-
//
|
|
2025
|
-
|
|
2026
|
-
|
|
2027
|
-
for (const pane of this.startupPanes) {
|
|
2028
|
-
this.kickedPanes.delete(pane);
|
|
2029
|
-
this.scheduleRedrawKick(pane, true);
|
|
2030
|
-
}
|
|
2031
|
-
}
|
|
2032
|
-
else if (this.startupPanes.length) {
|
|
2033
|
-
// 기동 구간이 끝났다 — 닫힌 pane 을 붙들고 있지 않도록 목록을 비운다
|
|
2034
|
-
this.startupPanes = [];
|
|
2035
|
-
}
|
|
2036
|
-
this.mainEl.style.flex = '0 0 ' + termW + 'px';
|
|
2037
|
-
this.mainEl.style.width = termW + 'px';
|
|
2038
|
-
this.mainEl.style.maxWidth = termW + 'px';
|
|
2916
|
+
this.lastSidebarW = extent;
|
|
2917
|
+
this.lastDock = side;
|
|
2918
|
+
// 기동 직후 흔들기(rearm kick)는 폐지했다 — 아래 relayout 주석 참고.
|
|
2919
|
+
// 사이드바는 창 위에 절대 배치한다. flex 자식으로 두면 도킹 방향을 바꿀 때마다
|
|
2920
|
+
// `.window` 의 flex-direction 을 뒤집어야 하고, 그러면 같은 줄에 있는 profile-tree 까지
|
|
2921
|
+
// 따라 움직인다. 절대 배치면 우리 것만 옮기면 된다.
|
|
2039
2922
|
if (this.sidebar) {
|
|
2040
|
-
this.sidebar.style
|
|
2923
|
+
const s = this.sidebar.style;
|
|
2924
|
+
s.position = 'absolute';
|
|
2925
|
+
s.left = side === 'right' ? 'auto' : '0';
|
|
2926
|
+
s.right = side === 'left' ? 'auto' : '0';
|
|
2927
|
+
s.top = side === 'bottom' ? 'auto' : '0';
|
|
2928
|
+
s.bottom = side === 'top' ? 'auto' : '0';
|
|
2929
|
+
s.width = horizontal ? extent + 'px' : 'auto';
|
|
2930
|
+
s.height = horizontal ? 'auto' : extent + 'px';
|
|
2931
|
+
for (const d of _dock__WEBPACK_IMPORTED_MODULE_11__.DOCK_SIDES) {
|
|
2932
|
+
this.sidebar.classList.toggle('ad-dock-' + d, d === side);
|
|
2933
|
+
}
|
|
2934
|
+
}
|
|
2935
|
+
// 터미널이 차지할 자리.
|
|
2936
|
+
//
|
|
2937
|
+
// 가로 도킹에서는 폭을 px 로 못 박는다. margin 만 주고 flex 에게 남는 폭을 맡겨 봤더니
|
|
2938
|
+
// (2026-09-01) xterm 이 fit 할 때 잡는 폭과 TUI 가 아는 폭이 어긋나 입력창이 깨졌다 —
|
|
2939
|
+
// 가로선과 입력 텍스트가 한 줄에 섞여 wrap 되는 그 증상이다. 폭을 직접 주면 xterm host 가
|
|
2940
|
+
// 재는 값이 곧 우리가 정한 값이라 어긋날 여지가 없다.
|
|
2941
|
+
// 세로 도킹은 폭을 건드릴 필요가 없고, `h-100`(height:100%) 때문에 margin 만으로는
|
|
2942
|
+
// 그만큼 창 밖으로 넘치므로 높이를 대신 깎는다.
|
|
2943
|
+
const m = this.mainEl.style;
|
|
2944
|
+
if (horizontal) {
|
|
2945
|
+
m.flex = '0 0 ' + termW + 'px';
|
|
2946
|
+
m.width = termW + 'px';
|
|
2947
|
+
m.maxWidth = termW + 'px';
|
|
2948
|
+
m.removeProperty('height');
|
|
2949
|
+
m.marginTop = '0';
|
|
2950
|
+
m.marginBottom = '0';
|
|
2951
|
+
// 왼쪽 도킹일 때만 앞자리를 비운다 (오른쪽 도킹은 고정폭만으로 자리가 남는다)
|
|
2952
|
+
m.marginLeft = side === 'left' ? extent + 'px' : '0';
|
|
2953
|
+
m.marginRight = '0';
|
|
2954
|
+
}
|
|
2955
|
+
else {
|
|
2956
|
+
m.removeProperty('flex');
|
|
2957
|
+
m.removeProperty('width');
|
|
2958
|
+
m.removeProperty('max-width');
|
|
2959
|
+
m.marginLeft = '0';
|
|
2960
|
+
m.marginRight = '0';
|
|
2961
|
+
m.marginTop = side === 'top' ? extent + 'px' : '0';
|
|
2962
|
+
m.marginBottom = side === 'bottom' ? extent + 'px' : '0';
|
|
2963
|
+
// **`important` 가 필요하다.** `.content.main` 에는 부트스트랩 `h-100` 클래스가 붙어
|
|
2964
|
+
// 있고 그 규칙이 `height: 100% !important` 라, 평범한 인라인 스타일은 진다.
|
|
2965
|
+
// 그래서 상하 도킹에서 터미널 높이가 전혀 줄지 않았다 — bottom 이면 아래쪽이
|
|
2966
|
+
// 사이드바에 가리고, top 이면 그만큼 창 밖으로 넘쳤다
|
|
2967
|
+
// (2026-09-02 실측, 창 1100x700 / 사이드바 200: 인라인은 `calc(100% - 200px)` 인데
|
|
2968
|
+
// computed 는 `700px`, host 는 top 에서 `1070x670@y=215` 로 하단 185px 가 잘렸다).
|
|
2969
|
+
m.setProperty('height', 'calc(100% - ' + extent + 'px)', 'important');
|
|
2041
2970
|
}
|
|
2971
|
+
(_a = this.dock) === null || _a === void 0 ? void 0 : _a.syncHandle();
|
|
2972
|
+
this.syncBackgroundClip();
|
|
2973
|
+
// 우리가 하는 일은 여기까지다 — **폭을 정해 `.content.main` 에 적고 fit 한 번.**
|
|
2974
|
+
//
|
|
2975
|
+
// 2026-09-02 방향 전환(유저 지적): "Tabby 는 창 크기가 어떻든 알아서 줄맞춤 되잖아?" 맞다.
|
|
2976
|
+
// 순정 경로는 xterm `ResizeObserver` → `fitAddon.fit()` → `session.resize()` 이고,
|
|
2977
|
+
// 우리가 폭만 정해 주면 그 뒤는 Tabby 가 스스로 계산한다.
|
|
2978
|
+
// 예전에는 "순정이 우리 CSS 변경을 못 따라온다"고 보고 보정을 4겹 쌓았다 —
|
|
2979
|
+
// relayout 시 fit / `watchSize`(250ms 마다 fit+resize, 6초) / `syncPtySize` 재전송 사슬 /
|
|
2980
|
+
// `nudgePtyRedraw`(cols-1↔cols 흔들기). 그 겹침이 순정 수렴과 경쟁하면서
|
|
2981
|
+
// 프레임을 반쯤 그린 상태를 남겼고, 그게 실사용 깨짐의 정체였다(항목 24~28 은 전부 그 부작용).
|
|
2982
|
+
// 전제도 로그로 반박됐다 — 폭은 탭이 생기기 전에 이미 확정돼 있는데도
|
|
2983
|
+
// (`rearm kick termW -1->2000 panes=0`) 새 탭 xterm 이 254 로 뜬다. 그건 레이아웃 확정 전의
|
|
2984
|
+
// 초기 측정값일 뿐이고 순정 옵저버가 어차피 수렴시킨다.
|
|
2985
|
+
// 그래서 자동 보정은 전부 걷어냈다. 남은 건 이 fit 하나(순정 경로를 깨우는 용도)다.
|
|
2042
2986
|
this.scheduleRefit();
|
|
2987
|
+
// 폭이 어긋나는 사고를 다시 만나면 이 줄이 실제 값을 알려 준다 —
|
|
2988
|
+
// 우리가 정한 값(termW)과 브라우저가 실제로 준 폭(clientWidth)이 다르면 그게 원인이다
|
|
2989
|
+
this.diag(`${new Date().toISOString()} relayout dock=${side} avail=${availW}x${availH}`
|
|
2990
|
+
+ ` extent=${extent} termW=${termW} mainW=${this.mainEl.clientWidth}`);
|
|
2043
2991
|
}
|
|
2044
2992
|
/**
|
|
2045
2993
|
* 폭을 바꿨으면 터미널에 반드시 알려야 한다.
|
|
@@ -2094,7 +3042,9 @@ let AgentDeckService = class AgentDeckService {
|
|
|
2094
3042
|
catch {
|
|
2095
3043
|
// 아직 붙지 않은 프론트엔드 — 다음 relayout 때 다시 잡힌다
|
|
2096
3044
|
}
|
|
2097
|
-
|
|
3045
|
+
// 여기서 끝낸다. 예전에는 뒤이어 `syncPtySize` 로 pty 에 직접 크기를 쐈는데,
|
|
3046
|
+
// fit() 이 이미 순정 경로(onResize → session.resize)를 태우므로 중복이고,
|
|
3047
|
+
// 그 중복이 순정 수렴과 경쟁해 화면을 깨뜨렸다 (2026-09-02).
|
|
2098
3048
|
}
|
|
2099
3049
|
}
|
|
2100
3050
|
}
|
|
@@ -2121,14 +3071,97 @@ let AgentDeckService = class AgentDeckService {
|
|
|
2121
3071
|
if (!session || !session.open || typeof session.resize !== 'function') {
|
|
2122
3072
|
return;
|
|
2123
3073
|
}
|
|
3074
|
+
// 이 경로는 지금까지 조용했다. 그래서 "언제 pty 폭이 바뀌었나" 가 로그에 안 남아
|
|
3075
|
+
// 어긋남 사고를 만나도 발생 지점을 못 짚었다 (watchSize 의 `size fix` 만 남았다).
|
|
3076
|
+
// 값이 실제로 달라질 때만 남긴다 — 같은 값 재전송은 매 refit 마다 일어나므로 시끄럽다.
|
|
3077
|
+
const prev = anyPane.size;
|
|
3078
|
+
if ((prev === null || prev === void 0 ? void 0 : prev.columns) !== cols || (prev === null || prev === void 0 ? void 0 : prev.rows) !== rows) {
|
|
3079
|
+
this.diag(`${new Date().toISOString()} pty resize ${prev === null || prev === void 0 ? void 0 : prev.columns}x${prev === null || prev === void 0 ? void 0 : prev.rows}`
|
|
3080
|
+
+ ` -> ${cols}x${rows}`);
|
|
3081
|
+
}
|
|
2124
3082
|
try {
|
|
2125
3083
|
session.resize(cols, rows);
|
|
2126
3084
|
anyPane.size = { columns: cols, rows };
|
|
2127
3085
|
}
|
|
2128
3086
|
catch {
|
|
2129
3087
|
// 세션이 막 닫혔다 — 다음 refit 때 다시 잡힌다
|
|
3088
|
+
return;
|
|
3089
|
+
}
|
|
3090
|
+
// **보냈다고 도착한 것이 아니다.**
|
|
3091
|
+
//
|
|
3092
|
+
// 세션이 막 열린 직후의 `session.resize()` 는 pty 에 닿지 못하고 유실된다
|
|
3093
|
+
// (2026-09-01 실측: `size fix 254x78 -> 280x78` + `pty resize` + `redraw nudge` 가
|
|
3094
|
+
// 모두 돌고 `pane.size` 도 280 으로 남았는데, 같은 탭 셸에 물어보니 `PTYW=254`.
|
|
3095
|
+
// 같은 호출을 몇 초 뒤에 CDP 로 하면 그때는 정상 반영된다 — 초기화 경합이다).
|
|
3096
|
+
// 그런데 `pane.size` 에 낙관적으로 280 을 적어 두므로, 크기 감시(watchSize)는
|
|
3097
|
+
// "이제 맞다" 고 보고 손을 뗀다. 그 뒤에 뜨는 TUI 는 254 폭으로 첫 화면을 그린다.
|
|
3098
|
+
//
|
|
3099
|
+
// 그래서 값이 실제로 바뀐 경우에만 같은 크기를 몇 번 더 보낸다. pty 크기가 이미
|
|
3100
|
+
// 그 값이면 아무 일도 일어나지 않으므로(SIGWINCH 도 안 난다) 재전송은 무해하다.
|
|
3101
|
+
if ((prev === null || prev === void 0 ? void 0 : prev.columns) !== cols || (prev === null || prev === void 0 ? void 0 : prev.rows) !== rows) {
|
|
3102
|
+
for (const delay of PTY_RESIZE_RETRY_MS) {
|
|
3103
|
+
setTimeout(() => {
|
|
3104
|
+
var _a, _b;
|
|
3105
|
+
const now = (_a = anyPane.frontend) === null || _a === void 0 ? void 0 : _a.xterm;
|
|
3106
|
+
// 그동안 폭이 또 바뀌었으면 그쪽 경로가 새 값을 보낸다 — 옛 값을 덮어쓰지 않는다
|
|
3107
|
+
if (!((_b = anyPane.session) === null || _b === void 0 ? void 0 : _b.open) || (now === null || now === void 0 ? void 0 : now.cols) !== cols || (now === null || now === void 0 ? void 0 : now.rows) !== rows) {
|
|
3108
|
+
return;
|
|
3109
|
+
}
|
|
3110
|
+
try {
|
|
3111
|
+
anyPane.session.resize(cols, rows);
|
|
3112
|
+
}
|
|
3113
|
+
catch {
|
|
3114
|
+
// 세션이 닫혔다
|
|
3115
|
+
}
|
|
3116
|
+
}, delay);
|
|
3117
|
+
}
|
|
3118
|
+
}
|
|
3119
|
+
}
|
|
3120
|
+
/**
|
|
3121
|
+
* pty 크기만 한 칸 줄였다 되돌려 TUI 가 화면을 다시 그리게 한다. **수동 복구 전용.**
|
|
3122
|
+
*
|
|
3123
|
+
* 자동 경로에서는 전부 걷어냈다 — 이 흔들기가 순정 수렴과 경쟁하며 깨진 화면을
|
|
3124
|
+
* 만들어 왔다(2026-09-02). 남겨 둔 이유는 `repairPane`(사이드바 `↻` / `agentdeck-repair`)
|
|
3125
|
+
* 때문이다. 거기서는 사용자가 "지금 깨졌으니 고쳐라" 를 명시로 눌렀고, 크기를 맞추는
|
|
3126
|
+
* 것만으로는 앱이 다시 그리지 않는 경우가 실제로 있다 — 폭이 **실제로 바뀌어야**
|
|
3127
|
+
* SIGWINCH 가 나가므로 한 칸 흔들어 두 번 내보낸다. 뒤이어 `Ctrl+L` 이 따라간다.
|
|
3128
|
+
*
|
|
3129
|
+
* 좁힌 값을 `pane.size` 에 그대로 적는다. 되돌리기가 유실됐는데 낙관적으로 `cols` 를
|
|
3130
|
+
* 적어 두면 아무도 어긋남을 못 보고 한 열 좁은 채로 굳는다(옛 버그).
|
|
3131
|
+
*/
|
|
3132
|
+
nudgePtyRedraw(pane) {
|
|
3133
|
+
var _a, _b, _c, _d;
|
|
3134
|
+
const anyPane = pane;
|
|
3135
|
+
const session = anyPane.session;
|
|
3136
|
+
const cols = (_b = (_a = anyPane.frontend) === null || _a === void 0 ? void 0 : _a.xterm) === null || _b === void 0 ? void 0 : _b.cols;
|
|
3137
|
+
const rows = (_d = (_c = anyPane.frontend) === null || _c === void 0 ? void 0 : _c.xterm) === null || _d === void 0 ? void 0 : _d.rows;
|
|
3138
|
+
if (!(session === null || session === void 0 ? void 0 : session.open) || typeof session.resize !== 'function' || !(cols > 1) || !(rows > 1)) {
|
|
3139
|
+
return;
|
|
3140
|
+
}
|
|
3141
|
+
this.diag(`${new Date().toISOString()} redraw nudge ${cols}x${rows}`);
|
|
3142
|
+
try {
|
|
3143
|
+
session.resize(cols - 1, rows);
|
|
3144
|
+
anyPane.size = { columns: cols - 1, rows };
|
|
3145
|
+
setTimeout(() => {
|
|
3146
|
+
var _a;
|
|
3147
|
+
if (!((_a = anyPane.session) === null || _a === void 0 ? void 0 : _a.open)) {
|
|
3148
|
+
return;
|
|
3149
|
+
}
|
|
3150
|
+
// 되돌릴 값은 "지금" 의 xterm 폭이다 — 흔드는 사이 사이드바가 움직였을 수 있다
|
|
3151
|
+
this.syncPtySize(pane, anyPane.frontend);
|
|
3152
|
+
}, 60);
|
|
3153
|
+
}
|
|
3154
|
+
catch {
|
|
3155
|
+
// 세션이 막 닫혔다
|
|
2130
3156
|
}
|
|
2131
3157
|
}
|
|
3158
|
+
// 크기 감시(`watchSize` / `watchAllSizes`)도 폐지했다 (2026-09-02).
|
|
3159
|
+
//
|
|
3160
|
+
// 250ms 마다 `fitAddon.fit()` + `session.resize()` 를 6초간 쏘던 장치다. 목적은
|
|
3161
|
+
// "xterm 은 넓어졌는데 pty 는 옛 폭" 을 잡는 것이었는데, 그 어긋남은 순정
|
|
3162
|
+
// ResizeObserver 가 이미 수렴시킨다. 우리 감시는 그 수렴 도중의 중간값을 보고
|
|
3163
|
+
// 계속 끼어들었고, 끝에 `nudgePtyRedraw` 흔들기까지 붙여 화면을 깨뜨렸다.
|
|
3164
|
+
// 순정이 정말 못 맞추는 경우가 나오면 `screenWatch` 채증에 남는다 — 그때 근거를 갖고 되켠다.
|
|
2132
3165
|
// ---------- 사이드바 DOM ----------
|
|
2133
3166
|
buildSidebar() {
|
|
2134
3167
|
const el = document.createElement('div');
|
|
@@ -2142,6 +3175,9 @@ let AgentDeckService = class AgentDeckService {
|
|
|
2142
3175
|
'<div class="ad-foot">',
|
|
2143
3176
|
' <button class="ad-btn ad-new" title="새 탭">+ 새 탭</button>',
|
|
2144
3177
|
' <button class="ad-btn ad-settings" title="설정">설정</button>',
|
|
3178
|
+
// 화면이 깨졌을 때 누르는 자리 — 창을 복원→최대화로 흔들던 것을 대신한다.
|
|
3179
|
+
// 자주 쓰는 두 버튼(새 탭·설정) 뒤에 둔다 (2026-09-01 유저 요청)
|
|
3180
|
+
' <button class="ad-btn ad-repair" title="화면 복구 (모든 탭 다시 그리기)">↻</button>',
|
|
2145
3181
|
'</div>',
|
|
2146
3182
|
].join('\n');
|
|
2147
3183
|
// 순정 탭바보다 뒤(오른쪽)에 놓는다 — 왼쪽에 두면 세로로 길어 허전하다는 피드백(2026-08-28)
|
|
@@ -2151,9 +3187,45 @@ let AgentDeckService = class AgentDeckService {
|
|
|
2151
3187
|
el.querySelector('.ad-new').addEventListener('click', () => {
|
|
2152
3188
|
this.zone.run(() => { void this.openNewTab(); });
|
|
2153
3189
|
});
|
|
3190
|
+
el.querySelector('.ad-repair').addEventListener('click', () => {
|
|
3191
|
+
this.zone.run(() => this.repair('all'));
|
|
3192
|
+
});
|
|
2154
3193
|
el.querySelector('.ad-settings').addEventListener('click', () => {
|
|
2155
3194
|
this.zone.run(() => this.openSettings());
|
|
2156
3195
|
});
|
|
3196
|
+
// 크기/도킹은 설정 창이 아니라 마우스로 맞춘다.
|
|
3197
|
+
// 드래그 중에는 relayout 이 초당 수십 번 불리므로 config.save() 는 손을 뗄 때만 부른다.
|
|
3198
|
+
this.dock = new _dock__WEBPACK_IMPORTED_MODULE_11__.DockController({
|
|
3199
|
+
sidebar: el,
|
|
3200
|
+
windowEl: this.windowEl,
|
|
3201
|
+
getDock: () => this.dockSide,
|
|
3202
|
+
setDock: side => {
|
|
3203
|
+
this.config.store.agentDeck.sidebarDock = side;
|
|
3204
|
+
},
|
|
3205
|
+
getSize: side => {
|
|
3206
|
+
const cfg = this.config.store.agentDeck;
|
|
3207
|
+
if (!(0,_dock__WEBPACK_IMPORTED_MODULE_11__.isHorizontalDock)(side)) {
|
|
3208
|
+
return cfg.sidebarHeight || 0;
|
|
3209
|
+
}
|
|
3210
|
+
// 지금 좌/우로 붙어 있다면 화면에 실제로 적용된 폭이 가장 정확하다
|
|
3211
|
+
// (자동 계산 상태면 sidebarWidth 가 0 이라 물어봐야 소용이 없다)
|
|
3212
|
+
return (0,_dock__WEBPACK_IMPORTED_MODULE_11__.isHorizontalDock)(this.dockSide) && this.lastSidebarW > 0
|
|
3213
|
+
? this.lastSidebarW
|
|
3214
|
+
: (cfg.sidebarWidth || 0);
|
|
3215
|
+
},
|
|
3216
|
+
setSize: px => {
|
|
3217
|
+
const cfg = this.config.store.agentDeck;
|
|
3218
|
+
if ((0,_dock__WEBPACK_IMPORTED_MODULE_11__.isHorizontalDock)(this.dockSide)) {
|
|
3219
|
+
cfg.sidebarWidth = px;
|
|
3220
|
+
}
|
|
3221
|
+
else {
|
|
3222
|
+
cfg.sidebarHeight = px;
|
|
3223
|
+
}
|
|
3224
|
+
},
|
|
3225
|
+
commit: () => this.zone.run(() => this.config.save()),
|
|
3226
|
+
relayout: () => this.relayout(),
|
|
3227
|
+
});
|
|
3228
|
+
this.dock.install();
|
|
2157
3229
|
}
|
|
2158
3230
|
/** 설정에 지정된 기본 프로필(기본값 agentdeck:root)로 새 탭을 연다 */
|
|
2159
3231
|
async openNewTab() {
|
|
@@ -2259,12 +3331,13 @@ let AgentDeckService = class AgentDeckService {
|
|
|
2259
3331
|
row.style.setProperty('--ad-color', style.color);
|
|
2260
3332
|
// [AD:...] 표식은 상태로 이미 반영했으니 제목에서는 지운다
|
|
2261
3333
|
const tabTitle = (0,_detect__WEBPACK_IMPORTED_MODULE_7__.stripTitleMarker)(tab.customTitle || tab.title || '');
|
|
2262
|
-
//
|
|
2263
|
-
//
|
|
2264
|
-
//
|
|
2265
|
-
//
|
|
2266
|
-
|
|
2267
|
-
|
|
3334
|
+
// 세 줄은 서로 다른 것을 말한다 —
|
|
3335
|
+
// 1) 제목: 에이전트가 세션에 붙인 이름. 세션이 사는 동안 거의 안 바뀐다
|
|
3336
|
+
// (Claude Code 는 시작 때 붙인 콘솔 제목을 계속 들고 있다, 2026-08-28 실측)
|
|
3337
|
+
// 2) 프롬프트: 마지막으로 보낸 지시. 작업이 넘어갈 때마다 바뀐다
|
|
3338
|
+
// 3) 상태: 지금 돌고 있는지 / 얼마나 됐는지
|
|
3339
|
+
// 예전에는 1과 2를 한 줄에 겹쳐 놓느라 둘 중 하나를 못 봤다.
|
|
3340
|
+
const title = tabTitle || '(제목 없음)';
|
|
2268
3341
|
const elapsed = this.config.store.agentDeck.showElapsed
|
|
2269
3342
|
? this.formatElapsed(Date.now() - st.since)
|
|
2270
3343
|
: '';
|
|
@@ -2272,9 +3345,11 @@ let AgentDeckService = class AgentDeckService {
|
|
|
2272
3345
|
'<div class="ad-bar"></div>',
|
|
2273
3346
|
'<div class="ad-body">',
|
|
2274
3347
|
' <div class="ad-title"></div>',
|
|
3348
|
+
// 라벨은 더블클릭하면 입력창으로 바뀐다(startLabelEdit) — 그때 줄이 무너지지 않게
|
|
3349
|
+
// 감싸는 줄을 따로 둔다
|
|
3350
|
+
' <div class="ad-prompt"><span class="ad-label"></span></div>',
|
|
2275
3351
|
' <div class="ad-meta">',
|
|
2276
3352
|
' <span class="ad-badge"></span>',
|
|
2277
|
-
' <span class="ad-label"></span>',
|
|
2278
3353
|
' <span class="ad-elapsed"></span>',
|
|
2279
3354
|
' </div>',
|
|
2280
3355
|
'</div>',
|
|
@@ -2282,8 +3357,11 @@ let AgentDeckService = class AgentDeckService {
|
|
|
2282
3357
|
].join('\n');
|
|
2283
3358
|
// textContent 로만 넣는다 — 탭 제목에 어떤 문자가 와도 마크업으로 해석되지 않게
|
|
2284
3359
|
row.querySelector('.ad-title').textContent = title;
|
|
2285
|
-
|
|
2286
|
-
|
|
3360
|
+
const labelEl = row.querySelector('.ad-label');
|
|
3361
|
+
// 아직 프롬프트가 없어도 줄은 남겨 둔다 — 줄 수가 들쭉날쭉하면 목록이 어지럽고,
|
|
3362
|
+
// 더블클릭으로 이름을 붙일 자리도 사라진다
|
|
3363
|
+
labelEl.textContent = st.label || '—';
|
|
3364
|
+
labelEl.classList.toggle('empty', !st.label);
|
|
2287
3365
|
row.querySelector('.ad-elapsed').textContent = elapsed;
|
|
2288
3366
|
const badge = row.querySelector('.ad-badge');
|
|
2289
3367
|
badge.textContent = style.icon + ' ' + style.label;
|
|
@@ -2358,6 +3436,11 @@ let AgentDeckService = class AgentDeckService {
|
|
|
2358
3436
|
return [label, action];
|
|
2359
3437
|
});
|
|
2360
3438
|
items.push(['↺ 자동 감지로', () => this.status.unpin(tab)]);
|
|
3439
|
+
// 이 탭만 다시 그리기 — 사이드바 아래 ↻ 는 모든 탭이 대상이라 무거울 때가 있다
|
|
3440
|
+
items.push(['↻ 화면 복구', () => {
|
|
3441
|
+
this.app.selectTab(tab);
|
|
3442
|
+
this.repair('active');
|
|
3443
|
+
}]);
|
|
2361
3444
|
for (const [label, action] of items) {
|
|
2362
3445
|
const item = document.createElement('div');
|
|
2363
3446
|
item.className = 'ad-menu-item';
|
|
@@ -2524,6 +3607,17 @@ const WAITING_RE = [
|
|
|
2524
3607
|
/\[y\/N\]/i,
|
|
2525
3608
|
/Press Enter to continue/i,
|
|
2526
3609
|
];
|
|
3610
|
+
/**
|
|
3611
|
+
* 에이전트가 작업 중일 때만 화면에 내보내는 문구.
|
|
3612
|
+
*
|
|
3613
|
+
* 상태줄 갱신 같은 평시 출력과 구분하는 근거다 — 이 문구는 일을 붙잡고 있는 동안에만 뜨고
|
|
3614
|
+
* 끝나면 사라지므로, 사라진 시점부터 시간을 재면 진행중에서 정확히 내려올 수 있다.
|
|
3615
|
+
*/
|
|
3616
|
+
const BUSY_RE = [
|
|
3617
|
+
/esc to interrupt/i,
|
|
3618
|
+
/ctrl\+c to (?:interrupt|cancel|stop)/i,
|
|
3619
|
+
/\btokens?\b.{0,20}\besc\b/i,
|
|
3620
|
+
];
|
|
2527
3621
|
const VALID = ['idle', 'running', 'waiting', 'done', 'error'];
|
|
2528
3622
|
/** OSC 가 청크 경계에 걸리는 것을 막기 위한 잔여 버퍼 */
|
|
2529
3623
|
const tails = new WeakMap();
|
|
@@ -2543,8 +3637,11 @@ function applyOutput(status, root, owner, data, autoDetect) {
|
|
|
2543
3637
|
const buf = ((_a = tails.get(owner)) !== null && _a !== void 0 ? _a : '') + data;
|
|
2544
3638
|
let matched = false;
|
|
2545
3639
|
let m;
|
|
3640
|
+
// 이미 처리한 OSC 가 어디까지인지 — 그 뒤만 다음 조각으로 넘긴다
|
|
3641
|
+
let consumed = 0;
|
|
2546
3642
|
OSC_RE.lastIndex = 0;
|
|
2547
3643
|
while ((m = OSC_RE.exec(buf)) !== null) {
|
|
3644
|
+
consumed = OSC_RE.lastIndex;
|
|
2548
3645
|
const s = m[1].toLowerCase();
|
|
2549
3646
|
if (VALID.includes(s)) {
|
|
2550
3647
|
status.setManual(root, s, m[2]);
|
|
@@ -2552,8 +3649,11 @@ function applyOutput(status, root, owner, data, autoDetect) {
|
|
|
2552
3649
|
matched = true;
|
|
2553
3650
|
}
|
|
2554
3651
|
}
|
|
2555
|
-
// 미완성 OSC 가 남아있을 수 있으므로 꼬리만
|
|
2556
|
-
|
|
3652
|
+
// 미완성 OSC 가 남아있을 수 있으므로 꼬리만 보관한다.
|
|
3653
|
+
// 단 이미 처리한 부분은 반드시 버린다 — 완결된 OSC 가 짧으면 꼬리 안에 통째로 남고,
|
|
3654
|
+
// 그러면 다음 출력마다 같은 시퀀스가 또 매치돼 상태가 그 값에 박혀 버린다
|
|
3655
|
+
// (2026-09-01, detect 테스트에서 드러남).
|
|
3656
|
+
tails.set(owner, buf.slice(Math.max(consumed, buf.length - 256)));
|
|
2557
3657
|
if (matched || !autoDetect) {
|
|
2558
3658
|
return;
|
|
2559
3659
|
}
|
|
@@ -2561,6 +3661,19 @@ function applyOutput(status, root, owner, data, autoDetect) {
|
|
|
2561
3661
|
status.setAuto(root, 'waiting');
|
|
2562
3662
|
return;
|
|
2563
3663
|
}
|
|
3664
|
+
// 에이전트가 "지금 일하는 중" 이라고 스스로 밝히는 표시를 찾는다
|
|
3665
|
+
if (BUSY_MARKS.some(mark => data.includes(mark)) || BUSY_RE.some(re => re.test(data))) {
|
|
3666
|
+
status.markBusy(root);
|
|
3667
|
+
status.setAuto(root, 'running');
|
|
3668
|
+
return;
|
|
3669
|
+
}
|
|
3670
|
+
// 그 표시를 내는 탭(에이전트 TUI)이라면, 표시가 없는 출력은 진행중의 근거가 아니다.
|
|
3671
|
+
// Claude Code 는 놀고 있을 때도 하단 상태줄을 매초 다시 그리기 때문에, 이 갈래가 없으면
|
|
3672
|
+
// 사이드바가 영원히 "진행중" 에 박혀 있게 된다 (2026-09-01 실측 — 세 탭 모두 진행중).
|
|
3673
|
+
if (status.isBusyMode(root)) {
|
|
3674
|
+
return;
|
|
3675
|
+
}
|
|
3676
|
+
// 신호를 낼 줄 모르는 보통 셸 — 예전처럼 "출력이 있다 = 진행중" 으로 본다
|
|
2564
3677
|
status.setAuto(root, 'running');
|
|
2565
3678
|
}
|
|
2566
3679
|
function forgetOutputBuffer(owner) {
|
|
@@ -2601,33 +3714,329 @@ function applyTitle(status, root, title, autoDetect) {
|
|
|
2601
3714
|
|
|
2602
3715
|
/***/ },
|
|
2603
3716
|
|
|
2604
|
-
/***/ "./src/
|
|
2605
|
-
|
|
2606
|
-
!*** ./src/
|
|
2607
|
-
|
|
3717
|
+
/***/ "./src/dock.ts"
|
|
3718
|
+
/*!*********************!*\
|
|
3719
|
+
!*** ./src/dock.ts ***!
|
|
3720
|
+
\*********************/
|
|
2608
3721
|
(__unused_webpack_module, __webpack_exports__, __webpack_require__) {
|
|
2609
3722
|
|
|
2610
3723
|
__webpack_require__.r(__webpack_exports__);
|
|
2611
3724
|
/* harmony export */ __webpack_require__.d(__webpack_exports__, {
|
|
2612
|
-
/* harmony export */
|
|
3725
|
+
/* harmony export */ DOCK_SIDES: () => (/* binding */ DOCK_SIDES),
|
|
3726
|
+
/* harmony export */ DockController: () => (/* binding */ DockController),
|
|
3727
|
+
/* harmony export */ isDockSide: () => (/* binding */ isDockSide),
|
|
3728
|
+
/* harmony export */ isHorizontalDock: () => (/* binding */ isHorizontalDock)
|
|
2613
3729
|
/* harmony export */ });
|
|
2614
|
-
/* harmony import */ var _angular_core__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! @angular/core */ "@angular/core");
|
|
2615
|
-
/* harmony import */ var _angular_core__WEBPACK_IMPORTED_MODULE_0___default = /*#__PURE__*/__webpack_require__.n(_angular_core__WEBPACK_IMPORTED_MODULE_0__);
|
|
2616
|
-
/* harmony import */ var tabby_core__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(/*! tabby-core */ "tabby-core");
|
|
2617
|
-
/* harmony import */ var tabby_core__WEBPACK_IMPORTED_MODULE_1___default = /*#__PURE__*/__webpack_require__.n(tabby_core__WEBPACK_IMPORTED_MODULE_1__);
|
|
2618
|
-
var __decorate = (undefined && undefined.__decorate) || function (decorators, target, key, desc) {
|
|
2619
|
-
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
2620
|
-
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
|
2621
|
-
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;
|
|
2622
|
-
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
2623
|
-
};
|
|
2624
|
-
|
|
2625
|
-
|
|
2626
3730
|
/**
|
|
2627
|
-
*
|
|
3731
|
+
* 사이드바를 마우스로 조절하는 부분 — 경계선 드래그로 크기, 헤더 드래그로 붙는 방향.
|
|
2628
3732
|
*
|
|
2629
|
-
*
|
|
2630
|
-
*
|
|
3733
|
+
* 설정 창에서 숫자를 넣게 하지 않고 Visual Studio 의 도킹처럼 직접 끌어서 맞추게 한다.
|
|
3734
|
+
* DOM 을 직접 만지는 코드라 deck.service 밖으로 빼 두고, 값을 읽고 쓰는 통로만
|
|
3735
|
+
* `DockHost` 로 받는다 (deck.service 가 config 와 relayout 을 쥐고 있으므로).
|
|
3736
|
+
*/
|
|
3737
|
+
const DOCK_SIDES = ['left', 'right', 'top', 'bottom'];
|
|
3738
|
+
function isDockSide(v) {
|
|
3739
|
+
return DOCK_SIDES.includes(v);
|
|
3740
|
+
}
|
|
3741
|
+
/** 좌/우 도킹이면 폭, 상/하 도킹이면 높이를 다룬다 */
|
|
3742
|
+
function isHorizontalDock(side) {
|
|
3743
|
+
return side === 'left' || side === 'right';
|
|
3744
|
+
}
|
|
3745
|
+
/** 드래그로 오해하지 않을 최소 이동 거리 (px) */
|
|
3746
|
+
const DRAG_THRESHOLD = 5;
|
|
3747
|
+
/** 존 버튼 밖에서 놓았을 때 가장자리로 인정하는 여백 비율 */
|
|
3748
|
+
const EDGE_RATIO = 0.25;
|
|
3749
|
+
class DockController {
|
|
3750
|
+
constructor(host) {
|
|
3751
|
+
this.host = host;
|
|
3752
|
+
this.handle = null;
|
|
3753
|
+
this.overlay = null;
|
|
3754
|
+
this.preview = null;
|
|
3755
|
+
this.zones = new Map();
|
|
3756
|
+
this.resizing = false;
|
|
3757
|
+
this.dragging = false;
|
|
3758
|
+
this.dragArmed = false;
|
|
3759
|
+
this.dragFrom = { x: 0, y: 0 };
|
|
3760
|
+
this.hoverSide = null;
|
|
3761
|
+
}
|
|
3762
|
+
/** 사이드바가 만들어진 뒤 한 번 부른다 */
|
|
3763
|
+
install() {
|
|
3764
|
+
this.installResizeHandle();
|
|
3765
|
+
this.installDockDrag();
|
|
3766
|
+
}
|
|
3767
|
+
/** 도킹 방향이 바뀌면 핸들 위치도 반대편으로 옮겨야 한다 */
|
|
3768
|
+
syncHandle() {
|
|
3769
|
+
if (!this.handle) {
|
|
3770
|
+
return;
|
|
3771
|
+
}
|
|
3772
|
+
const side = this.host.getDock();
|
|
3773
|
+
this.handle.className = 'ad-resize ad-resize-' + side;
|
|
3774
|
+
}
|
|
3775
|
+
// ---------- 경계선 드래그 (크기) ----------
|
|
3776
|
+
installResizeHandle() {
|
|
3777
|
+
const handle = document.createElement('div');
|
|
3778
|
+
handle.className = 'ad-resize';
|
|
3779
|
+
this.host.sidebar.appendChild(handle);
|
|
3780
|
+
this.handle = handle;
|
|
3781
|
+
this.syncHandle();
|
|
3782
|
+
handle.addEventListener('pointerdown', (ev) => {
|
|
3783
|
+
// 왼쪽 버튼만
|
|
3784
|
+
if (ev.button !== 0) {
|
|
3785
|
+
return;
|
|
3786
|
+
}
|
|
3787
|
+
ev.preventDefault();
|
|
3788
|
+
ev.stopPropagation();
|
|
3789
|
+
this.resizing = true;
|
|
3790
|
+
handle.setPointerCapture(ev.pointerId);
|
|
3791
|
+
document.body.classList.add('ad-resizing');
|
|
3792
|
+
});
|
|
3793
|
+
handle.addEventListener('pointermove', (ev) => {
|
|
3794
|
+
if (!this.resizing) {
|
|
3795
|
+
return;
|
|
3796
|
+
}
|
|
3797
|
+
const side = this.host.getDock();
|
|
3798
|
+
const rect = this.host.windowEl.getBoundingClientRect();
|
|
3799
|
+
// 커서와 창 가장자리 사이의 거리가 곧 사이드바 크기다
|
|
3800
|
+
let size;
|
|
3801
|
+
if (side === 'right') {
|
|
3802
|
+
size = rect.right - ev.clientX;
|
|
3803
|
+
}
|
|
3804
|
+
else if (side === 'left') {
|
|
3805
|
+
size = ev.clientX - rect.left;
|
|
3806
|
+
}
|
|
3807
|
+
else if (side === 'bottom') {
|
|
3808
|
+
size = rect.bottom - ev.clientY;
|
|
3809
|
+
}
|
|
3810
|
+
else {
|
|
3811
|
+
size = ev.clientY - rect.top;
|
|
3812
|
+
}
|
|
3813
|
+
this.host.setSize(Math.round(size));
|
|
3814
|
+
this.host.relayout();
|
|
3815
|
+
});
|
|
3816
|
+
const end = (ev) => {
|
|
3817
|
+
if (!this.resizing) {
|
|
3818
|
+
return;
|
|
3819
|
+
}
|
|
3820
|
+
this.resizing = false;
|
|
3821
|
+
try {
|
|
3822
|
+
handle.releasePointerCapture(ev.pointerId);
|
|
3823
|
+
}
|
|
3824
|
+
catch {
|
|
3825
|
+
// 이미 놓쳤다 — 무시해도 상태는 위에서 정리됐다
|
|
3826
|
+
}
|
|
3827
|
+
document.body.classList.remove('ad-resizing');
|
|
3828
|
+
this.host.commit();
|
|
3829
|
+
};
|
|
3830
|
+
handle.addEventListener('pointerup', end);
|
|
3831
|
+
handle.addEventListener('pointercancel', end);
|
|
3832
|
+
}
|
|
3833
|
+
// ---------- 헤더 드래그 (도킹 방향) ----------
|
|
3834
|
+
installDockDrag() {
|
|
3835
|
+
const head = this.host.sidebar.querySelector('.ad-head');
|
|
3836
|
+
if (!head) {
|
|
3837
|
+
return;
|
|
3838
|
+
}
|
|
3839
|
+
head.addEventListener('pointerdown', (ev) => {
|
|
3840
|
+
if (ev.button !== 0) {
|
|
3841
|
+
return;
|
|
3842
|
+
}
|
|
3843
|
+
// 헤더 안의 버튼류를 누른 것이면 건드리지 않는다
|
|
3844
|
+
if (ev.target.closest('button, input')) {
|
|
3845
|
+
return;
|
|
3846
|
+
}
|
|
3847
|
+
this.dragArmed = true;
|
|
3848
|
+
this.dragFrom = { x: ev.clientX, y: ev.clientY };
|
|
3849
|
+
head.setPointerCapture(ev.pointerId);
|
|
3850
|
+
});
|
|
3851
|
+
head.addEventListener('pointermove', (ev) => {
|
|
3852
|
+
if (!this.dragArmed) {
|
|
3853
|
+
return;
|
|
3854
|
+
}
|
|
3855
|
+
if (!this.dragging) {
|
|
3856
|
+
const dx = ev.clientX - this.dragFrom.x;
|
|
3857
|
+
const dy = ev.clientY - this.dragFrom.y;
|
|
3858
|
+
if (Math.hypot(dx, dy) < DRAG_THRESHOLD) {
|
|
3859
|
+
return;
|
|
3860
|
+
}
|
|
3861
|
+
// 여기서부터 도킹 드래그다 — 가이드를 띄운다
|
|
3862
|
+
this.dragging = true;
|
|
3863
|
+
this.showOverlay();
|
|
3864
|
+
}
|
|
3865
|
+
this.updateHover(ev.clientX, ev.clientY);
|
|
3866
|
+
});
|
|
3867
|
+
const end = (ev) => {
|
|
3868
|
+
if (!this.dragArmed) {
|
|
3869
|
+
return;
|
|
3870
|
+
}
|
|
3871
|
+
this.dragArmed = false;
|
|
3872
|
+
try {
|
|
3873
|
+
head.releasePointerCapture(ev.pointerId);
|
|
3874
|
+
}
|
|
3875
|
+
catch {
|
|
3876
|
+
// 무시
|
|
3877
|
+
}
|
|
3878
|
+
if (!this.dragging) {
|
|
3879
|
+
return;
|
|
3880
|
+
}
|
|
3881
|
+
this.dragging = false;
|
|
3882
|
+
const side = this.hoverSide;
|
|
3883
|
+
this.hideOverlay();
|
|
3884
|
+
if (side && side !== this.host.getDock()) {
|
|
3885
|
+
this.host.setDock(side);
|
|
3886
|
+
this.host.relayout();
|
|
3887
|
+
this.syncHandle();
|
|
3888
|
+
this.host.commit();
|
|
3889
|
+
}
|
|
3890
|
+
};
|
|
3891
|
+
head.addEventListener('pointerup', end);
|
|
3892
|
+
head.addEventListener('pointercancel', end);
|
|
3893
|
+
}
|
|
3894
|
+
// ---------- 드롭 가이드 ----------
|
|
3895
|
+
showOverlay() {
|
|
3896
|
+
if (this.overlay) {
|
|
3897
|
+
return;
|
|
3898
|
+
}
|
|
3899
|
+
const overlay = document.createElement('div');
|
|
3900
|
+
overlay.className = 'ad-dock-overlay';
|
|
3901
|
+
const preview = document.createElement('div');
|
|
3902
|
+
preview.className = 'ad-dock-preview';
|
|
3903
|
+
overlay.appendChild(preview);
|
|
3904
|
+
for (const side of DOCK_SIDES) {
|
|
3905
|
+
const zone = document.createElement('div');
|
|
3906
|
+
zone.className = 'ad-dock-zone ad-dock-zone-' + side;
|
|
3907
|
+
zone.innerHTML = '<span class="ad-dock-mark"></span>';
|
|
3908
|
+
overlay.appendChild(zone);
|
|
3909
|
+
this.zones.set(side, zone);
|
|
3910
|
+
}
|
|
3911
|
+
document.body.appendChild(overlay);
|
|
3912
|
+
this.overlay = overlay;
|
|
3913
|
+
this.preview = preview;
|
|
3914
|
+
this.positionZones();
|
|
3915
|
+
}
|
|
3916
|
+
/** 존 버튼을 `.window` 네 가장자리 안쪽에 놓는다 (창이 전체화면이 아닐 수도 있으므로 매번 잰다) */
|
|
3917
|
+
positionZones() {
|
|
3918
|
+
const rect = this.host.windowEl.getBoundingClientRect();
|
|
3919
|
+
const cx = rect.left + rect.width / 2;
|
|
3920
|
+
const cy = rect.top + rect.height / 2;
|
|
3921
|
+
const inset = 56;
|
|
3922
|
+
const place = {
|
|
3923
|
+
left: [rect.left + inset, cy],
|
|
3924
|
+
right: [rect.right - inset, cy],
|
|
3925
|
+
top: [cx, rect.top + inset],
|
|
3926
|
+
bottom: [cx, rect.bottom - inset],
|
|
3927
|
+
};
|
|
3928
|
+
for (const side of DOCK_SIDES) {
|
|
3929
|
+
const zone = this.zones.get(side);
|
|
3930
|
+
if (!zone) {
|
|
3931
|
+
continue;
|
|
3932
|
+
}
|
|
3933
|
+
const [x, y] = place[side];
|
|
3934
|
+
zone.style.left = x + 'px';
|
|
3935
|
+
zone.style.top = y + 'px';
|
|
3936
|
+
}
|
|
3937
|
+
}
|
|
3938
|
+
/** 커서 위치로 어느 쪽에 붙일지 정하고 미리보기를 그린다 */
|
|
3939
|
+
updateHover(x, y) {
|
|
3940
|
+
const side = this.hitTest(x, y);
|
|
3941
|
+
if (side !== this.hoverSide) {
|
|
3942
|
+
this.hoverSide = side;
|
|
3943
|
+
for (const [s, zone] of this.zones) {
|
|
3944
|
+
zone.classList.toggle('active', s === side);
|
|
3945
|
+
}
|
|
3946
|
+
}
|
|
3947
|
+
this.drawPreview(side);
|
|
3948
|
+
}
|
|
3949
|
+
/**
|
|
3950
|
+
* 존 버튼 위에 있으면 그쪽, 아니면 커서가 창의 어느 가장자리 띠에 들어와 있는지로 정한다.
|
|
3951
|
+
* 가운데에 있으면 null — 놓아도 아무 일이 없다.
|
|
3952
|
+
*/
|
|
3953
|
+
hitTest(x, y) {
|
|
3954
|
+
for (const [side, zone] of this.zones) {
|
|
3955
|
+
const r = zone.getBoundingClientRect();
|
|
3956
|
+
if (x >= r.left && x <= r.right && y >= r.top && y <= r.bottom) {
|
|
3957
|
+
return side;
|
|
3958
|
+
}
|
|
3959
|
+
}
|
|
3960
|
+
const rect = this.host.windowEl.getBoundingClientRect();
|
|
3961
|
+
const dx = (x - rect.left) / rect.width;
|
|
3962
|
+
const dy = (y - rect.top) / rect.height;
|
|
3963
|
+
if (dx < EDGE_RATIO && dx <= dy && dx <= 1 - dy) {
|
|
3964
|
+
return 'left';
|
|
3965
|
+
}
|
|
3966
|
+
if (dx > 1 - EDGE_RATIO && 1 - dx <= dy && 1 - dx <= 1 - dy) {
|
|
3967
|
+
return 'right';
|
|
3968
|
+
}
|
|
3969
|
+
if (dy < EDGE_RATIO) {
|
|
3970
|
+
return 'top';
|
|
3971
|
+
}
|
|
3972
|
+
if (dy > 1 - EDGE_RATIO) {
|
|
3973
|
+
return 'bottom';
|
|
3974
|
+
}
|
|
3975
|
+
return null;
|
|
3976
|
+
}
|
|
3977
|
+
/** 놓으면 사이드바가 어디에 얼마만큼 자리잡는지 반투명 박스로 미리 보여준다 */
|
|
3978
|
+
drawPreview(side) {
|
|
3979
|
+
if (!this.preview) {
|
|
3980
|
+
return;
|
|
3981
|
+
}
|
|
3982
|
+
if (!side) {
|
|
3983
|
+
this.preview.style.display = 'none';
|
|
3984
|
+
return;
|
|
3985
|
+
}
|
|
3986
|
+
const rect = this.host.windowEl.getBoundingClientRect();
|
|
3987
|
+
const horizontal = isHorizontalDock(side);
|
|
3988
|
+
// 방향마다 기억된 크기가 다르다 — 지금 붙어 있는 쪽이 아니라 놓으려는 쪽 값을 물어본다
|
|
3989
|
+
const current = this.host.getSize(side);
|
|
3990
|
+
const size = horizontal
|
|
3991
|
+
? Math.min(Math.max(current || 320, 180), rect.width * 0.6)
|
|
3992
|
+
: Math.min(Math.max(current || 200, 120), rect.height * 0.6);
|
|
3993
|
+
const s = this.preview.style;
|
|
3994
|
+
s.display = 'block';
|
|
3995
|
+
s.left = (side === 'right' ? rect.right - size : rect.left) + 'px';
|
|
3996
|
+
s.top = (side === 'bottom' ? rect.bottom - size : rect.top) + 'px';
|
|
3997
|
+
s.width = (horizontal ? size : rect.width) + 'px';
|
|
3998
|
+
s.height = (horizontal ? rect.height : size) + 'px';
|
|
3999
|
+
}
|
|
4000
|
+
hideOverlay() {
|
|
4001
|
+
var _a;
|
|
4002
|
+
(_a = this.overlay) === null || _a === void 0 ? void 0 : _a.remove();
|
|
4003
|
+
this.overlay = null;
|
|
4004
|
+
this.preview = null;
|
|
4005
|
+
this.zones.clear();
|
|
4006
|
+
this.hoverSide = null;
|
|
4007
|
+
}
|
|
4008
|
+
}
|
|
4009
|
+
|
|
4010
|
+
|
|
4011
|
+
/***/ },
|
|
4012
|
+
|
|
4013
|
+
/***/ "./src/hotkeys.ts"
|
|
4014
|
+
/*!************************!*\
|
|
4015
|
+
!*** ./src/hotkeys.ts ***!
|
|
4016
|
+
\************************/
|
|
4017
|
+
(__unused_webpack_module, __webpack_exports__, __webpack_require__) {
|
|
4018
|
+
|
|
4019
|
+
__webpack_require__.r(__webpack_exports__);
|
|
4020
|
+
/* harmony export */ __webpack_require__.d(__webpack_exports__, {
|
|
4021
|
+
/* harmony export */ AgentDeckHotkeyProvider: () => (/* binding */ AgentDeckHotkeyProvider)
|
|
4022
|
+
/* harmony export */ });
|
|
4023
|
+
/* harmony import */ var _angular_core__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! @angular/core */ "@angular/core");
|
|
4024
|
+
/* harmony import */ var _angular_core__WEBPACK_IMPORTED_MODULE_0___default = /*#__PURE__*/__webpack_require__.n(_angular_core__WEBPACK_IMPORTED_MODULE_0__);
|
|
4025
|
+
/* harmony import */ var tabby_core__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(/*! tabby-core */ "tabby-core");
|
|
4026
|
+
/* harmony import */ var tabby_core__WEBPACK_IMPORTED_MODULE_1___default = /*#__PURE__*/__webpack_require__.n(tabby_core__WEBPACK_IMPORTED_MODULE_1__);
|
|
4027
|
+
var __decorate = (undefined && undefined.__decorate) || function (decorators, target, key, desc) {
|
|
4028
|
+
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
4029
|
+
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
|
4030
|
+
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;
|
|
4031
|
+
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
4032
|
+
};
|
|
4033
|
+
|
|
4034
|
+
|
|
4035
|
+
/**
|
|
4036
|
+
* 핫키 "선언"만 한다.
|
|
4037
|
+
*
|
|
4038
|
+
* 여기서 HotkeysService 를 주입하면 안 된다 — HotkeysService 자신이
|
|
4039
|
+
* HotkeyProvider 목록을 주입받으므로(tabby-core/src/services/hotkeys.service.ts:85)
|
|
2631
4040
|
* 순환 의존이 되어 부분 초기화된 인스턴스가 넘어온다.
|
|
2632
4041
|
* 실제 핫키 처리는 AgentDeckService 에서 구독한다.
|
|
2633
4042
|
*/
|
|
@@ -2646,6 +4055,10 @@ let AgentDeckHotkeyProvider = class AgentDeckHotkeyProvider extends tabby_core__
|
|
|
2646
4055
|
id: 'agentdeck-paste',
|
|
2647
4056
|
name: 'AgentDeck 붙여넣기 (텍스트는 그대로, 이미지는 Alt+V 로 위임)',
|
|
2648
4057
|
},
|
|
4058
|
+
{
|
|
4059
|
+
id: 'agentdeck-repair',
|
|
4060
|
+
name: 'AgentDeck 화면 복구 (깨진 TUI 다시 그리기)',
|
|
4061
|
+
},
|
|
2649
4062
|
];
|
|
2650
4063
|
}
|
|
2651
4064
|
};
|
|
@@ -2680,6 +4093,7 @@ __webpack_require__.r(__webpack_exports__);
|
|
|
2680
4093
|
/* harmony import */ var path__WEBPACK_IMPORTED_MODULE_5__ = __webpack_require__(/*! path */ "path");
|
|
2681
4094
|
/* harmony import */ var path__WEBPACK_IMPORTED_MODULE_5___default = /*#__PURE__*/__webpack_require__.n(path__WEBPACK_IMPORTED_MODULE_5__);
|
|
2682
4095
|
/* harmony import */ var _status_service__WEBPACK_IMPORTED_MODULE_6__ = __webpack_require__(/*! ./status.service */ "./src/status.service.ts");
|
|
4096
|
+
/* harmony import */ var _claudeHooks__WEBPACK_IMPORTED_MODULE_7__ = __webpack_require__(/*! ./claudeHooks */ "./src/claudeHooks.ts");
|
|
2683
4097
|
var __decorate = (undefined && undefined.__decorate) || function (decorators, target, key, desc) {
|
|
2684
4098
|
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
2685
4099
|
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
|
@@ -2696,12 +4110,17 @@ var __metadata = (undefined && undefined.__metadata) || function (k, v) {
|
|
|
2696
4110
|
|
|
2697
4111
|
|
|
2698
4112
|
|
|
4113
|
+
|
|
2699
4114
|
/**
|
|
2700
|
-
*
|
|
4115
|
+
* 에이전트 훅 -> 사이드바 상태 채널. **기본으로 켜져 있다** (`notifyChannel`).
|
|
2701
4116
|
*
|
|
2702
|
-
*
|
|
2703
|
-
*
|
|
2704
|
-
*
|
|
4117
|
+
* 이쪽이 상태의 정확한 원천이다 — 에이전트가 "지시를 받았다 / 승인을 기다린다 / 끝났다" 를
|
|
4118
|
+
* 직접 알려준다. 출력 패턴으로 추측하는 경로(detect.ts)는 훅이 없는 환경을 위한 폴백이고,
|
|
4119
|
+
* TUI 가 화면을 어떻게 그리는지에 기대는 만큼 흔들린다 (2026-09-01: Claude Code 가 놀 때도
|
|
4120
|
+
* 상태줄을 매초 갱신해 진행중이 안 풀렸다).
|
|
4121
|
+
*
|
|
4122
|
+
* 훅은 Claude Code 쪽 설정에 있어야 해서 플러그인 설치만으로는 걸리지 않는다. 그래서
|
|
4123
|
+
* 기동할 때 한 번 물어보고(promptHooksOnce), 설정 창에도 설치 버튼을 둔다(claudeHooks.ts).
|
|
2705
4124
|
*
|
|
2706
4125
|
* 왜 파일인가 — 콘솔 제목 방식(`hooks/agentdeck-status.ps1`)은 **훅에서는 통하지 않는다**.
|
|
2707
4126
|
* Claude Code 는 훅을 파이프 stdio 로 띄우기 때문에 그 자식 프로세스는 터미널의 콘솔에
|
|
@@ -2725,10 +4144,11 @@ var __metadata = (undefined && undefined.__metadata) || function (k, v) {
|
|
|
2725
4144
|
* 한 번 묶으면 그 뒤로는 활성 탭이 바뀌어도 같은 탭을 따라간다.
|
|
2726
4145
|
*/
|
|
2727
4146
|
let WorkNotifyService = class WorkNotifyService {
|
|
2728
|
-
constructor(app, config, status) {
|
|
4147
|
+
constructor(app, config, status, platform) {
|
|
2729
4148
|
this.app = app;
|
|
2730
4149
|
this.config = config;
|
|
2731
4150
|
this.status = status;
|
|
4151
|
+
this.platform = platform;
|
|
2732
4152
|
this.root = path__WEBPACK_IMPORTED_MODULE_5__.join(process.env.LOCALAPPDATA || path__WEBPACK_IMPORTED_MODULE_5__.join(os__WEBPACK_IMPORTED_MODULE_4__.homedir(), 'AppData', 'Local'), 'tabby-agentdeck');
|
|
2733
4153
|
this.dir = path__WEBPACK_IMPORTED_MODULE_5__.join(this.root, 'status');
|
|
2734
4154
|
/** 훅이 접속할 포트를 알려주는 파일 */
|
|
@@ -2743,11 +4163,75 @@ let WorkNotifyService = class WorkNotifyService {
|
|
|
2743
4163
|
/** 폴링을 시작한 시각 — 이보다 오래된 파일은 지난 실행의 잔재다 */
|
|
2744
4164
|
this.startedAt = 0;
|
|
2745
4165
|
}
|
|
4166
|
+
/**
|
|
4167
|
+
* 훅이 안 걸려 있으면 기동할 때 한 번 물어본다.
|
|
4168
|
+
*
|
|
4169
|
+
* 훅은 Claude Code 쪽 설정(`~/.claude/settings.json`)에 있어야 해서 플러그인을 설치하는 것만으로는
|
|
4170
|
+
* 걸리지 않는다. 그렇다고 말없이 남의 설정을 고칠 수는 없으니, 한 번 묻고 고른 대로 한다.
|
|
4171
|
+
* "다시 묻지 않기" 를 고르면 그 뒤로는 조용하고, 설정 창의 설치 버튼은 언제든 그대로 쓸 수 있다.
|
|
4172
|
+
*/
|
|
4173
|
+
async promptHooksOnce() {
|
|
4174
|
+
var _a;
|
|
4175
|
+
const cfg = this.config.store.agentDeck;
|
|
4176
|
+
if (cfg.hookPromptDismissed || !(0,_claudeHooks__WEBPACK_IMPORTED_MODULE_7__.hooksSupported)()) {
|
|
4177
|
+
return;
|
|
4178
|
+
}
|
|
4179
|
+
try {
|
|
4180
|
+
if ((0,_claudeHooks__WEBPACK_IMPORTED_MODULE_7__.hooksInstalled)()) {
|
|
4181
|
+
return;
|
|
4182
|
+
}
|
|
4183
|
+
}
|
|
4184
|
+
catch {
|
|
4185
|
+
// 설정을 못 읽었다 — 그 상태로 남의 파일을 고치자고 권할 수는 없다
|
|
4186
|
+
return;
|
|
4187
|
+
}
|
|
4188
|
+
const r = await this.platform.showMessageBox({
|
|
4189
|
+
// 이 타입에는 'question' 이 없다 (tabby-core MessageBoxOptions) — 경고로 띄우면
|
|
4190
|
+
// 필요 이상으로 겁을 주므로 'warning' 대신 형만 맞추고 문구로 성격을 드러낸다
|
|
4191
|
+
type: 'warning',
|
|
4192
|
+
message: 'AgentDeck — Claude Code 상태 통보를 연결할까요?',
|
|
4193
|
+
detail: [
|
|
4194
|
+
'연결하면 에이전트가 "지시를 받았다 / 승인을 기다린다 / 끝났다" 를 사이드바에 직접 알려줍니다.',
|
|
4195
|
+
'연결하지 않아도 동작은 하지만, 화면에 그려진 글자를 보고 추측하는 방식이라 상태가 부정확할 수 있습니다.',
|
|
4196
|
+
'',
|
|
4197
|
+
`설치 위치: ${(0,_claudeHooks__WEBPACK_IMPORTED_MODULE_7__.claudeSettingsPath)()}`,
|
|
4198
|
+
'기존 훅은 건드리지 않고, 쓰기 전에 같은 폴더에 백업을 남깁니다. 설정 창에서 언제든 제거할 수 있습니다.',
|
|
4199
|
+
].join('\n'),
|
|
4200
|
+
buttons: ['연결한다', '나중에', '다시 묻지 않기'],
|
|
4201
|
+
defaultId: 0,
|
|
4202
|
+
cancelId: 1,
|
|
4203
|
+
});
|
|
4204
|
+
if (r.response === 0) {
|
|
4205
|
+
try {
|
|
4206
|
+
(0,_claudeHooks__WEBPACK_IMPORTED_MODULE_7__.installHooks)();
|
|
4207
|
+
if (!cfg.notifyChannel) {
|
|
4208
|
+
cfg.notifyChannel = true;
|
|
4209
|
+
}
|
|
4210
|
+
this.config.save();
|
|
4211
|
+
}
|
|
4212
|
+
catch (e) {
|
|
4213
|
+
await this.platform.showMessageBox({
|
|
4214
|
+
type: 'error',
|
|
4215
|
+
message: 'AgentDeck — 훅을 설치하지 못했습니다.',
|
|
4216
|
+
detail: `${(_a = e === null || e === void 0 ? void 0 : e.message) !== null && _a !== void 0 ? _a : e}`,
|
|
4217
|
+
buttons: ['확인'],
|
|
4218
|
+
});
|
|
4219
|
+
}
|
|
4220
|
+
return;
|
|
4221
|
+
}
|
|
4222
|
+
if (r.response === 2) {
|
|
4223
|
+
cfg.hookPromptDismissed = true;
|
|
4224
|
+
this.config.save();
|
|
4225
|
+
}
|
|
4226
|
+
}
|
|
2746
4227
|
init() {
|
|
2747
4228
|
this.app.ready$.subscribe(() => {
|
|
2748
|
-
//
|
|
2749
|
-
//
|
|
2750
|
-
//
|
|
4229
|
+
// 훅이 안 걸려 있으면 기동할 때 한 번 물어본다. 통로만 열려 있고 보고자가 없으면
|
|
4230
|
+
// 아무 일도 일어나지 않는데, 사용자 눈에는 그냥 "상태가 이상하다" 로만 보인다.
|
|
4231
|
+
// 기동 직후는 화면이 정신없으니 조금 뒤에 띄운다
|
|
4232
|
+
setTimeout(() => { void this.promptHooksOnce(); }, 3000);
|
|
4233
|
+
// 채널이 닫혀 있으면 훅이 보낸 보고가 어디에도 닿지 않는다.
|
|
4234
|
+
// 기본은 열려 있고, 비용은 유휴 포트 하나와 400ms 폴링뿐이다
|
|
2751
4235
|
if (!this.config.store.agentDeck.notifyChannel) {
|
|
2752
4236
|
return;
|
|
2753
4237
|
}
|
|
@@ -2919,7 +4403,7 @@ let WorkNotifyService = class WorkNotifyService {
|
|
|
2919
4403
|
// 제목을 라벨 원천으로 쓸 수 있는지 보려고 한동안 비워 뒀고, 판정은 났다 —
|
|
2920
4404
|
// Claude Code 는 세션 첫 작업을 요약해 제목에 한 번 박고 그 뒤로 갱신하지 않는다.
|
|
2921
4405
|
// 그래서 라벨은 Enter 시점 입력창에서 직접 읽고(claimEnterLabel), 사이드바는
|
|
2922
|
-
//
|
|
4406
|
+
// 제목과 그 라벨을 각자 줄에 나눠 그린다(renderTab). 여기서 제목을 덮어쓸 이유가 없다.
|
|
2923
4407
|
}
|
|
2924
4408
|
/**
|
|
2925
4409
|
* 세션에 묶인 탭을 찾는다. 처음 보는 세션이면 지금 활성 탭에 묶는다.
|
|
@@ -2951,7 +4435,8 @@ WorkNotifyService = __decorate([
|
|
|
2951
4435
|
(0,_angular_core__WEBPACK_IMPORTED_MODULE_0__.Injectable)({ providedIn: 'root' }),
|
|
2952
4436
|
__metadata("design:paramtypes", [tabby_core__WEBPACK_IMPORTED_MODULE_1__.AppService,
|
|
2953
4437
|
tabby_core__WEBPACK_IMPORTED_MODULE_1__.ConfigService,
|
|
2954
|
-
_status_service__WEBPACK_IMPORTED_MODULE_6__.WorkStatusService
|
|
4438
|
+
_status_service__WEBPACK_IMPORTED_MODULE_6__.WorkStatusService,
|
|
4439
|
+
tabby_core__WEBPACK_IMPORTED_MODULE_1__.PlatformService])
|
|
2955
4440
|
], WorkNotifyService);
|
|
2956
4441
|
|
|
2957
4442
|
|
|
@@ -3017,9 +4502,9 @@ let AgentDeckProfileService = class AgentDeckProfileService {
|
|
|
3017
4502
|
args: [...(cfg.rootProfileArgs || [])],
|
|
3018
4503
|
cwd: cfg.rootProfileCwd,
|
|
3019
4504
|
env: { ...(cfg.rootProfileEnv || {}) },
|
|
3020
|
-
//
|
|
3021
|
-
//
|
|
3022
|
-
|
|
4505
|
+
// 권한 승격(runAsAdministrator)은 이 플러그인이 다루지 않는다 — Tabby 를 어떤 권한으로
|
|
4506
|
+
// 띄울지는 사용자의 몫이다. 단 프로필에 runAsAdministrator 를 켜면 Tabby 가 셸을
|
|
4507
|
+
// UAC.exe 릴레이로 감싸고 그 릴레이가 폭 1칸을 잃어 TUI 입력창이 깨진다(README 알려진 문제).
|
|
3023
4508
|
},
|
|
3024
4509
|
};
|
|
3025
4510
|
if (existing) {
|
|
@@ -3161,6 +4646,477 @@ function extractPrompt(lines) {
|
|
|
3161
4646
|
}
|
|
3162
4647
|
|
|
3163
4648
|
|
|
4649
|
+
/***/ },
|
|
4650
|
+
|
|
4651
|
+
/***/ "./src/screen.ts"
|
|
4652
|
+
/*!***********************!*\
|
|
4653
|
+
!*** ./src/screen.ts ***!
|
|
4654
|
+
\***********************/
|
|
4655
|
+
(__unused_webpack_module, __webpack_exports__, __webpack_require__) {
|
|
4656
|
+
|
|
4657
|
+
__webpack_require__.r(__webpack_exports__);
|
|
4658
|
+
/* harmony export */ __webpack_require__.d(__webpack_exports__, {
|
|
4659
|
+
/* harmony export */ judgeScreen: () => (/* binding */ judgeScreen)
|
|
4660
|
+
/* harmony export */ });
|
|
4661
|
+
/**
|
|
4662
|
+
* 터미널 화면이 깨졌는지 판정한다.
|
|
4663
|
+
*
|
|
4664
|
+
* "깨졌다" 의 정체는 하나다 — **TUI 가 아는 폭과 xterm 이 가진 폭이 다르다.**
|
|
4665
|
+
* 그러면 TUI 가 그린 한 행이 xterm 에서 두 행으로 접히거나(폭이 더 크다),
|
|
4666
|
+
* 행 오른쪽에 옛 프레임이 남는다(폭이 더 작다). 눈으로는 "입력창 구분선이 중간에서 끊긴다",
|
|
4667
|
+
* "가로선과 입력 텍스트가 한 줄에 섞인다" 로 보인다.
|
|
4668
|
+
*
|
|
4669
|
+
* 정상 화면의 모양은 실측해서 확정했다 (2026-09-01, Claude Code 2.1.x, CDP 로 xterm 버퍼 원문).
|
|
4670
|
+
* 사이드바를 켠 창(cols=280)과 순정 창(cols=360) 양쪽에서 완전히 같은 구조가 나왔다.
|
|
4671
|
+
*
|
|
4672
|
+
* 71 wrapped=0 len=278 " … ● high · /effort" ← 오른쪽 정렬 힌트
|
|
4673
|
+
* 72 wrapped=0 len=280 "──────────────────────────────────────────────────" ← len == cols
|
|
4674
|
+
* 73 wrapped=1 len= 1 "❯" ← wrapped=1 이 정상
|
|
4675
|
+
* 74 wrapped=0 len=280 "──────────────────────────────────────────────────" ← len == cols
|
|
4676
|
+
* 75 wrapped=0 len=155 " ⚠ Transcript saving is off — …"
|
|
4677
|
+
* 76 wrapped=0 len= 58 " ⏱ 1s 컨텍스트 사용량: …"
|
|
4678
|
+
* 77 wrapped=0 len= 38 " ⏵⏵ auto mode on (shift+tab to cycle)"
|
|
4679
|
+
*
|
|
4680
|
+
* 여기서 놓치기 쉬운 세 가지 —
|
|
4681
|
+
* - 가로선은 **cols 를 정확히 꽉 채운다**. 그래서 그 다음 줄에 `wrapped=1` 이 붙는 것은
|
|
4682
|
+
* xterm 의 pending-wrap 이고 **정상이다**. `wrapped` 만 보고 깨졌다고 판정하면 안 된다.
|
|
4683
|
+
* - 프롬프트 오른쪽에 오는 힌트(`● high · /effort`, `Try "how does …"`)는 **별도 줄**이다.
|
|
4684
|
+
* 프롬프트 줄과 같은 줄에 두 개의 머리(`❯`/`>`)가 보이면 그것이 깨진 화면이다.
|
|
4685
|
+
* - **폭이 다 맞아도 깨진 화면이 있다.** 테두리 두 줄이 맞붙어 `❯` 가 들어갈 행이
|
|
4686
|
+
* 사라지는 모양인데, 두 테두리 모두 cols 를 꽉 채우므로 폭 기준 판정은 전부 통과한다
|
|
4687
|
+
* (2026-09-02: 사람 눈으로도 이 도구로도 "정상" 이라고 오판했다). 그래서 폭만이 아니라
|
|
4688
|
+
* **입력할 자리가 있는가**(테두리 사이 간격 ≥ 2)를 같이 본다.
|
|
4689
|
+
*
|
|
4690
|
+
* xterm 없이 검증할 수 있도록 줄 배열만 받는 순수 함수로 둔다.
|
|
4691
|
+
*/
|
|
4692
|
+
/**
|
|
4693
|
+
* 가로선만으로 된 줄 (입력창 위/아래 테두리).
|
|
4694
|
+
* 두 글자까지 내려 잡는 이유 — 테두리가 화면 폭을 넘치면 다음 줄에 한두 칸짜리
|
|
4695
|
+
* 자투리로 떨어진다 (실측: cols=280 화면에 280칸 + `──` 2칸).
|
|
4696
|
+
*/
|
|
4697
|
+
const RULE_RE = /^[─━═]{2,}$/;
|
|
4698
|
+
/** 입력창 머리글자 — prompt.ts 와 같은 집합. 뒤가 줄 끝이어도(빈 입력창) 머리다 */
|
|
4699
|
+
const HEAD_RE = /^[>❯›❭➜](\s|$)/;
|
|
4700
|
+
/** 줄 어딘가에 있는 머리글자 — 가로선과 섞였는지 볼 때 쓴다 */
|
|
4701
|
+
const HEAD_ANYWHERE_RE = /[>❯›❭➜](\s|$)/;
|
|
4702
|
+
/** 선택지 커서(`❯ 1. Yes`)는 입력창 머리가 아니다 */
|
|
4703
|
+
const CHOICE_RE = /^[>❯›❭➜]\s*(\d+[.)]\s|(?:Yes|No)\b)/i;
|
|
4704
|
+
/** 가로선 문자 */
|
|
4705
|
+
const RULE_CHAR_RE = /[─━═]/;
|
|
4706
|
+
function judgeScreen(lines, cols) {
|
|
4707
|
+
const reasons = [];
|
|
4708
|
+
if (!(cols > 1) || !lines.length) {
|
|
4709
|
+
return { broken: false, reasons: [] };
|
|
4710
|
+
}
|
|
4711
|
+
const rules = [];
|
|
4712
|
+
const heads = [];
|
|
4713
|
+
for (let i = 0; i < lines.length; i++) {
|
|
4714
|
+
const line = lines[i];
|
|
4715
|
+
const body = line.text.replace(/\s+$/, '');
|
|
4716
|
+
if (!body) {
|
|
4717
|
+
continue;
|
|
4718
|
+
}
|
|
4719
|
+
if (RULE_RE.test(body)) {
|
|
4720
|
+
rules.push(i);
|
|
4721
|
+
// 테두리는 화면 폭을 꽉 채운다. 짧으면 TUI 가 더 좁은 폭으로 그린 것이고,
|
|
4722
|
+
// 그보다 짧은 자투리 줄이면 넘쳐 접힌 꼬리다 — 어느 쪽이든 폭이 어긋났다.
|
|
4723
|
+
//
|
|
4724
|
+
// 2026-09-02: `cols - 1` 도 정상으로 봐주도록 한 번 완화했다가 되돌렸다.
|
|
4725
|
+
// 근거로 삼은 `len=279` 스냅샷이 **전부 깨짐으로 판정된 화면**이었고,
|
|
4726
|
+
// 같은 로그의 정상 표본(`auto-after 복구됨`)은 `len == cols == 280` 이었다.
|
|
4727
|
+
// 병상 표본을 정상 기준으로 삼으면 안 된다.
|
|
4728
|
+
if (body.length !== cols) {
|
|
4729
|
+
reasons.push(`rule@${i} len=${body.length} != cols=${cols}`);
|
|
4730
|
+
}
|
|
4731
|
+
continue;
|
|
4732
|
+
}
|
|
4733
|
+
// 가로선과 다른 글자가 한 줄에 섞였다.
|
|
4734
|
+
// 상자형 입력창(`╭──╮` / `│ > … │`)을 쓰는 앱도 있으므로, 머리글자가 같은 줄에
|
|
4735
|
+
// 함께 있을 때만 깨진 것으로 본다 — 그것이 "가로선과 입력 텍스트가 섞인" 그 증상이다.
|
|
4736
|
+
if (RULE_CHAR_RE.test(body) && HEAD_ANYWHERE_RE.test(body)) {
|
|
4737
|
+
reasons.push(`mixed@${i} ${JSON.stringify(body.slice(0, 40))}`);
|
|
4738
|
+
}
|
|
4739
|
+
if (HEAD_RE.test(body) && !CHOICE_RE.test(body)) {
|
|
4740
|
+
heads.push(i);
|
|
4741
|
+
}
|
|
4742
|
+
}
|
|
4743
|
+
// 입력창 **안에** 머리가 두 개 이상 보인다 = 옛 프레임이 지워지지 않았다.
|
|
4744
|
+
//
|
|
4745
|
+
// 화면 전체의 머리 수를 세면 안 된다 — Claude Code 는 지난 사용자 프롬프트를
|
|
4746
|
+
// `❯ 내가 보낸 말` 로 대화 이력에 남기고, 큐에 쌓인 메시지도 같은 모양으로 나열한다.
|
|
4747
|
+
// 그래서 정상 화면에도 머리가 예사로 일곱 개 보인다 (2026-09-01 실측: `heads=7` 오탐으로
|
|
4748
|
+
// 자동 복구가 2초마다 헛돌았다). 판정은 입력창 테두리(가로선) 사이 구간에서만 한다.
|
|
4749
|
+
//
|
|
4750
|
+
// 판정은 **맨 아래 두 테두리** 사이에서만 한다. 위쪽 테두리를 `rules[0]` 으로 잡으면
|
|
4751
|
+
// 대화 이력에 스크롤되어 남은 옛 입력창의 테두리를 물어, 그 사이의 지난 프롬프트가
|
|
4752
|
+
// 전부 머리로 세어진다. 입력창은 언제나 화면 맨 아래에 있다.
|
|
4753
|
+
// 자투리 테두리(폭이 안 맞는 줄)는 위치 기준이 못 되므로 **화면 폭을 꽉 채운 것**만 쓴다.
|
|
4754
|
+
const fullRules = rules.filter(i => lines[i].text.replace(/\s+$/, '').length === cols);
|
|
4755
|
+
if (fullRules.length >= 2) {
|
|
4756
|
+
const bottom = fullRules[fullRules.length - 1];
|
|
4757
|
+
const top = fullRules[fullRules.length - 2];
|
|
4758
|
+
// **입력할 자리가 없다** — 테두리 두 줄이 맞붙어 `❯` 가 들어갈 행이 사라졌다.
|
|
4759
|
+
//
|
|
4760
|
+
// 이것이 2026-09-02 에 사람 눈으로도 도구로도 놓친 그 증상이다. 폭은 멀쩡해서
|
|
4761
|
+
// (테두리 둘 다 cols 를 꽉 채운다) 폭 기준 판정은 전부 통과해 버린다. 실측 픽셀:
|
|
4762
|
+
// 가로선 y=59 와 y=76 이 행높이(17px) 1행 간격, 그 사이 커서 글리프가 아래 선을 관통.
|
|
4763
|
+
// 정상이라면 두 테두리는 최소 한 행(입력줄)을 사이에 두므로 간격이 2 이상이다.
|
|
4764
|
+
if (bottom - top < 2) {
|
|
4765
|
+
reasons.push(`입력창에 자리가 없다 (테두리 ${top}·${bottom} 이 맞붙었다)`);
|
|
4766
|
+
}
|
|
4767
|
+
else {
|
|
4768
|
+
const inside = heads.filter(i => i > top && i < bottom).length;
|
|
4769
|
+
// 머리가 0 개인 것은 깨짐으로 보지 않는다 — 선택지 커서(`❯ 1. Yes`)만 있는
|
|
4770
|
+
// 정상 화면이 그렇게 보인다 (CHOICE_RE 로 빼고 나면 머리가 0 이 된다).
|
|
4771
|
+
if (inside > 1) {
|
|
4772
|
+
reasons.push(`입력창 안 머리=${inside} (기대 1)`);
|
|
4773
|
+
}
|
|
4774
|
+
}
|
|
4775
|
+
}
|
|
4776
|
+
// **아래 테두리가 통째로 사라졌다.**
|
|
4777
|
+
//
|
|
4778
|
+
// 2026-09-02 실사용 실측(창 캡처 픽셀 스캔, 행높이 17px): 가로선이 `y=1295` 하나뿐이고
|
|
4779
|
+
// 프롬프트 글리프(y≈1304~1320) 아래 `y≈1329` 에 있어야 할 두 번째 가로선이 없었다.
|
|
4780
|
+
// 이 모양은 위 규칙들이 전부 통과한다 — 남은 테두리 하나는 폭을 꽉 채우고(폭 규칙 통과),
|
|
4781
|
+
// 테두리가 하나뿐이라 `fullRules.length >= 2` 가 거짓이라 자리 규칙도 건너뛰고,
|
|
4782
|
+
// `rules.length > 1` 도 거짓이라 섞임 규칙도 안 본다. 판정의 구멍이었다.
|
|
4783
|
+
//
|
|
4784
|
+
// 머리 **바로 위** 가 폭을 꽉 채운 테두리일 때만 본다. 그래야 3줄 입력창(`──/❯/──`)을
|
|
4785
|
+
// 쓰는 화면에만 적용되고, 테두리가 없는 일반 셸이나 상자형 입력창은 건드리지 않는다.
|
|
4786
|
+
// 대화 이력에 남은 옛 `❯ 내가 보낸 말` 도 안전하다 — 그 아래에는 현재 입력창의
|
|
4787
|
+
// 테두리가 있으므로 "아래에 테두리가 없다" 조건에 걸리지 않는다.
|
|
4788
|
+
if (heads.length && fullRules.length) {
|
|
4789
|
+
const head = heads[heads.length - 1];
|
|
4790
|
+
const topAttached = fullRules.includes(head - 1);
|
|
4791
|
+
const belowHead = fullRules.some(i => i > head);
|
|
4792
|
+
if (topAttached && !belowHead) {
|
|
4793
|
+
reasons.push(`입력창 아래 테두리가 없다 (머리@${head}, 위 테두리@${head - 1})`);
|
|
4794
|
+
}
|
|
4795
|
+
}
|
|
4796
|
+
// 테두리 길이가 서로 다르면 두 폭이 화면에 공존한다
|
|
4797
|
+
if (rules.length > 1) {
|
|
4798
|
+
const lens = new Set(rules.map(i => lines[i].text.replace(/\s+$/, '').length));
|
|
4799
|
+
if (lens.size > 1) {
|
|
4800
|
+
reasons.push(`rule lens=${[...lens].join(',')} (섞임)`);
|
|
4801
|
+
}
|
|
4802
|
+
}
|
|
4803
|
+
return { broken: reasons.length > 0, reasons };
|
|
4804
|
+
}
|
|
4805
|
+
|
|
4806
|
+
|
|
4807
|
+
/***/ },
|
|
4808
|
+
|
|
4809
|
+
/***/ "./src/settings.component.ts"
|
|
4810
|
+
/*!***********************************!*\
|
|
4811
|
+
!*** ./src/settings.component.ts ***!
|
|
4812
|
+
\***********************************/
|
|
4813
|
+
(__unused_webpack_module, __webpack_exports__, __webpack_require__) {
|
|
4814
|
+
|
|
4815
|
+
__webpack_require__.r(__webpack_exports__);
|
|
4816
|
+
/* harmony export */ __webpack_require__.d(__webpack_exports__, {
|
|
4817
|
+
/* harmony export */ AgentDeckSettingsTabComponent: () => (/* binding */ AgentDeckSettingsTabComponent)
|
|
4818
|
+
/* harmony export */ });
|
|
4819
|
+
/* harmony import */ var _angular_core__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! @angular/core */ "@angular/core");
|
|
4820
|
+
/* harmony import */ var _angular_core__WEBPACK_IMPORTED_MODULE_0___default = /*#__PURE__*/__webpack_require__.n(_angular_core__WEBPACK_IMPORTED_MODULE_0__);
|
|
4821
|
+
/* harmony import */ var tabby_core__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(/*! tabby-core */ "tabby-core");
|
|
4822
|
+
/* harmony import */ var tabby_core__WEBPACK_IMPORTED_MODULE_1___default = /*#__PURE__*/__webpack_require__.n(tabby_core__WEBPACK_IMPORTED_MODULE_1__);
|
|
4823
|
+
/* harmony import */ var _config__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(/*! ./config */ "./src/config.ts");
|
|
4824
|
+
/* harmony import */ var _claudeHooks__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(/*! ./claudeHooks */ "./src/claudeHooks.ts");
|
|
4825
|
+
var __decorate = (undefined && undefined.__decorate) || function (decorators, target, key, desc) {
|
|
4826
|
+
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
4827
|
+
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
|
4828
|
+
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;
|
|
4829
|
+
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
4830
|
+
};
|
|
4831
|
+
var __metadata = (undefined && undefined.__metadata) || function (k, v) {
|
|
4832
|
+
if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
|
|
4833
|
+
};
|
|
4834
|
+
|
|
4835
|
+
|
|
4836
|
+
|
|
4837
|
+
|
|
4838
|
+
const DOCK_LABELS = {
|
|
4839
|
+
left: '왼쪽',
|
|
4840
|
+
right: '오른쪽',
|
|
4841
|
+
top: '위쪽',
|
|
4842
|
+
bottom: '아래쪽',
|
|
4843
|
+
};
|
|
4844
|
+
/**
|
|
4845
|
+
* 설정 > AgentDeck 탭.
|
|
4846
|
+
*
|
|
4847
|
+
* 사용자가 실제로 바꿀 일이 있는 두 가지만 노출한다 — 레이아웃 on/off 와 작업 루트 프로필.
|
|
4848
|
+
* 나머지(화면비, 사이드바 폭, 투명도, 상태 감지, 입력 가로채기, 렌더 보정, 통보 채널)는
|
|
4849
|
+
* 손댈 이유가 거의 없고 잘못 만지면 화면이 깨지는 값이라 `config.ts` 의 기본값으로 고정하고
|
|
4850
|
+
* 설정 창에서는 뺐다. 정말 필요하면 설정 파일의 `agentDeck.*` 를 직접 고친다.
|
|
4851
|
+
*
|
|
4852
|
+
* `enabled` 는 deck.service 가 `config.changed$` 를 구독해 즉시 반영한다(deck.service.ts:256).
|
|
4853
|
+
* 루트 프로필은 기동 시 한 번만 읽으므로 재시작이 필요하다고 적어 둔다.
|
|
4854
|
+
*/
|
|
4855
|
+
let AgentDeckSettingsTabComponent = class AgentDeckSettingsTabComponent {
|
|
4856
|
+
constructor(config, platform) {
|
|
4857
|
+
this.config = config;
|
|
4858
|
+
this.platform = platform;
|
|
4859
|
+
/** 이 환경에서 훅을 걸 수 있나 (통보 스크립트가 PowerShell 이라 Windows 전용) */
|
|
4860
|
+
this.hooksSupported = (0,_claudeHooks__WEBPACK_IMPORTED_MODULE_3__.hooksSupported)();
|
|
4861
|
+
/** 지금 걸려 있나 — 파일을 매번 읽지 않도록 화면용으로 들고 있는다 */
|
|
4862
|
+
this.hooksOn = false;
|
|
4863
|
+
/** 훅을 쓰다 실패한 이유 — 조용히 삼키면 사용자는 버튼이 먹통인 줄 안다 */
|
|
4864
|
+
this.hookError = '';
|
|
4865
|
+
this.refreshHooks();
|
|
4866
|
+
}
|
|
4867
|
+
refreshHooks() {
|
|
4868
|
+
var _a;
|
|
4869
|
+
try {
|
|
4870
|
+
this.hooksOn = this.hooksSupported && (0,_claudeHooks__WEBPACK_IMPORTED_MODULE_3__.hooksInstalled)();
|
|
4871
|
+
}
|
|
4872
|
+
catch (e) {
|
|
4873
|
+
this.hooksOn = false;
|
|
4874
|
+
this.hookError = `설정을 읽지 못했다: ${(_a = e === null || e === void 0 ? void 0 : e.message) !== null && _a !== void 0 ? _a : e}`;
|
|
4875
|
+
}
|
|
4876
|
+
}
|
|
4877
|
+
/**
|
|
4878
|
+
* 훅을 걸거나 뗀다.
|
|
4879
|
+
*
|
|
4880
|
+
* 걸 때는 통보 채널도 같이 켠다 — 훅만 걸고 채널이 닫혀 있으면 보고가 어디에도 닿지 않는데,
|
|
4881
|
+
* 사용자 입장에서는 "설치했는데 아무 일도 안 일어난다" 로만 보인다.
|
|
4882
|
+
*/
|
|
4883
|
+
toggleHooks() {
|
|
4884
|
+
var _a;
|
|
4885
|
+
this.hookError = '';
|
|
4886
|
+
try {
|
|
4887
|
+
if (this.hooksOn) {
|
|
4888
|
+
(0,_claudeHooks__WEBPACK_IMPORTED_MODULE_3__.uninstallHooks)();
|
|
4889
|
+
}
|
|
4890
|
+
else {
|
|
4891
|
+
(0,_claudeHooks__WEBPACK_IMPORTED_MODULE_3__.installHooks)();
|
|
4892
|
+
if (!this.config.store.agentDeck.notifyChannel) {
|
|
4893
|
+
this.config.store.agentDeck.notifyChannel = true;
|
|
4894
|
+
this.config.save();
|
|
4895
|
+
}
|
|
4896
|
+
}
|
|
4897
|
+
}
|
|
4898
|
+
catch (e) {
|
|
4899
|
+
this.hookError = `${(_a = e === null || e === void 0 ? void 0 : e.message) !== null && _a !== void 0 ? _a : e}`;
|
|
4900
|
+
}
|
|
4901
|
+
this.refreshHooks();
|
|
4902
|
+
}
|
|
4903
|
+
/**
|
|
4904
|
+
* OS 폴더 선택 창을 띄워 작업 폴더를 고른다.
|
|
4905
|
+
*
|
|
4906
|
+
* `pickDirectory` 는 취소하면 null/undefined 를 주므로 그때는 기존 값을 건드리지 않는다.
|
|
4907
|
+
* 다이얼로그 제목은 Tabby 버전에 따라 인자를 받기도 하고 안 받기도 해서 any 로 넘긴다 —
|
|
4908
|
+
* 안 받는 쪽은 그냥 무시하므로 어느 쪽에서도 깨지지 않는다.
|
|
4909
|
+
* 돌려받은 경로는 백슬래시를 슬래시로 바꿔 저장한다. 설정 파일에 그대로 적히는 값이라
|
|
4910
|
+
* 손으로 고칠 때 이스케이프 사고가 나지 않게 하려는 것이고, Windows 도 슬래시 경로를 받는다.
|
|
4911
|
+
*/
|
|
4912
|
+
/** 배치가 기본값에서 벗어나 있나 — 벗어나 있을 때만 초기화 버튼이 살아 있다 */
|
|
4913
|
+
layoutChanged() {
|
|
4914
|
+
const cfg = this.config.store.agentDeck;
|
|
4915
|
+
return Object.keys(_config__WEBPACK_IMPORTED_MODULE_2__.LAYOUT_DEFAULTS).some(k => cfg[k] !== _config__WEBPACK_IMPORTED_MODULE_2__.LAYOUT_DEFAULTS[k]);
|
|
4916
|
+
}
|
|
4917
|
+
dockLabel() {
|
|
4918
|
+
var _a;
|
|
4919
|
+
return (_a = DOCK_LABELS[this.config.store.agentDeck.sidebarDock]) !== null && _a !== void 0 ? _a : '오른쪽';
|
|
4920
|
+
}
|
|
4921
|
+
sizeLabel() {
|
|
4922
|
+
const cfg = this.config.store.agentDeck;
|
|
4923
|
+
if (cfg.sidebarDock === 'top' || cfg.sidebarDock === 'bottom') {
|
|
4924
|
+
return '높이 ' + cfg.sidebarHeight + 'px';
|
|
4925
|
+
}
|
|
4926
|
+
return cfg.sidebarWidth ? '폭 ' + cfg.sidebarWidth + 'px' : '폭 자동';
|
|
4927
|
+
}
|
|
4928
|
+
/**
|
|
4929
|
+
* 도킹 방향과 크기만 기본값으로 되돌린다.
|
|
4930
|
+
*
|
|
4931
|
+
* 되돌리기 쉬운 조작이라 확인 창은 두지 않는다. `config.save()` 가 `changed$` 를 발화시키고
|
|
4932
|
+
* deck.service 가 그걸 받아 곧바로 다시 배치하므로(deck.service.ts:256) 재시작도 필요 없다.
|
|
4933
|
+
*/
|
|
4934
|
+
resetLayout() {
|
|
4935
|
+
Object.assign(this.config.store.agentDeck, _config__WEBPACK_IMPORTED_MODULE_2__.LAYOUT_DEFAULTS);
|
|
4936
|
+
this.config.save();
|
|
4937
|
+
}
|
|
4938
|
+
async pickCwd() {
|
|
4939
|
+
const picked = await this.platform.pickDirectory('작업 폴더 선택');
|
|
4940
|
+
if (!picked) {
|
|
4941
|
+
return;
|
|
4942
|
+
}
|
|
4943
|
+
this.config.store.agentDeck.rootProfileCwd = String(picked).replace(/\\/g, '/');
|
|
4944
|
+
this.config.save();
|
|
4945
|
+
}
|
|
4946
|
+
};
|
|
4947
|
+
AgentDeckSettingsTabComponent = __decorate([
|
|
4948
|
+
(0,_angular_core__WEBPACK_IMPORTED_MODULE_0__.Component)({
|
|
4949
|
+
selector: 'agentdeck-settings-tab',
|
|
4950
|
+
template: `
|
|
4951
|
+
<h3 class="mb-3">AgentDeck</h3>
|
|
4952
|
+
|
|
4953
|
+
<div class="form-line">
|
|
4954
|
+
<div class="header">
|
|
4955
|
+
<div class="title">레이아웃 사용</div>
|
|
4956
|
+
<div class="description">끄면 사이드바와 고정폭 레이아웃이 사라지고 Tabby 순정 화면으로 돌아간다.</div>
|
|
4957
|
+
</div>
|
|
4958
|
+
<toggle [(ngModel)]="config.store.agentDeck.enabled" (ngModelChange)="config.save()"></toggle>
|
|
4959
|
+
</div>
|
|
4960
|
+
|
|
4961
|
+
<div class="form-line">
|
|
4962
|
+
<div class="header">
|
|
4963
|
+
<div class="title">UI 배치 초기화</div>
|
|
4964
|
+
<div class="description">
|
|
4965
|
+
마우스로 바꾼 사이드바 위치와 크기를 처음 상태로 되돌린다 —
|
|
4966
|
+
오른쪽 도킹 + 화면비로 자동 계산한 폭.
|
|
4967
|
+
<span *ngIf="layoutChanged()">지금: {{dockLabel()}} 도킹, {{sizeLabel()}}.</span>
|
|
4968
|
+
<span *ngIf="!layoutChanged()">지금은 기본 상태다.</span>
|
|
4969
|
+
</div>
|
|
4970
|
+
</div>
|
|
4971
|
+
<button class="btn btn-secondary" type="button" [disabled]="!layoutChanged()" (click)="resetLayout()">
|
|
4972
|
+
<i class="fas fa-undo me-2"></i>초기화
|
|
4973
|
+
</button>
|
|
4974
|
+
</div>
|
|
4975
|
+
|
|
4976
|
+
<h3 class="mt-4 mb-3">Claude Code 연동</h3>
|
|
4977
|
+
<div class="text-muted mb-3">
|
|
4978
|
+
에이전트가 "지시를 받았다 / 승인을 기다린다 / 끝났다" 를 직접 알려주게 한다.
|
|
4979
|
+
이게 사이드바 상태의 가장 정확한 원천이다 — 걸지 않으면 화면에 그려진 글자를 보고
|
|
4980
|
+
추측하는 방식으로 동작하고, 그만큼 판정이 흔들린다.
|
|
4981
|
+
</div>
|
|
4982
|
+
|
|
4983
|
+
<div class="form-line" *ngIf="!hooksSupported">
|
|
4984
|
+
<div class="header">
|
|
4985
|
+
<div class="title">이 환경에서는 걸 수 없다</div>
|
|
4986
|
+
<div class="description">통보 스크립트가 PowerShell 이라 Windows 에서만 동작한다. 상태는 출력 패턴으로 추측한다.</div>
|
|
4987
|
+
</div>
|
|
4988
|
+
</div>
|
|
4989
|
+
|
|
4990
|
+
<div class="form-line" *ngIf="hooksSupported">
|
|
4991
|
+
<div class="header">
|
|
4992
|
+
<div class="title">상태 통보 훅</div>
|
|
4993
|
+
<div class="description">
|
|
4994
|
+
{{hooksOn ? '설치됨' : '설치 안 됨'}} — <code>~/.claude/settings.json</code> 의
|
|
4995
|
+
UserPromptSubmit / Notification / Stop 에 걸린다. 다른 훅은 건드리지 않고,
|
|
4996
|
+
쓰기 전에 같은 폴더에 백업을 남긴다.
|
|
4997
|
+
<span *ngIf="hookError" class="text-danger d-block mt-1">{{hookError}}</span>
|
|
4998
|
+
</div>
|
|
4999
|
+
</div>
|
|
5000
|
+
<button class="btn" type="button"
|
|
5001
|
+
[class.btn-secondary]="!hooksOn" [class.btn-outline-danger]="hooksOn"
|
|
5002
|
+
(click)="toggleHooks()">
|
|
5003
|
+
<i class="fas me-2" [class.fa-plug]="!hooksOn" [class.fa-trash]="hooksOn"></i>
|
|
5004
|
+
{{hooksOn ? '제거' : '설치'}}
|
|
5005
|
+
</button>
|
|
5006
|
+
</div>
|
|
5007
|
+
|
|
5008
|
+
<div class="form-line" *ngIf="hooksSupported && hooksOn">
|
|
5009
|
+
<div class="header">
|
|
5010
|
+
<div class="title">통보 채널</div>
|
|
5011
|
+
<div class="description">훅이 보낸 상태를 받는 통로. 꺼져 있으면 훅을 걸어도 전달되지 않는다. 변경은 Tabby 재시작 후 적용된다.</div>
|
|
5012
|
+
</div>
|
|
5013
|
+
<toggle [(ngModel)]="config.store.agentDeck.notifyChannel" (ngModelChange)="config.save()"></toggle>
|
|
5014
|
+
</div>
|
|
5015
|
+
|
|
5016
|
+
<h3 class="mt-4 mb-3">작업 루트 프로필</h3>
|
|
5017
|
+
<div class="text-muted mb-3">새 탭이 항상 같은 작업 폴더에서 뜨도록 전용 프로필을 만든다. 변경은 Tabby 재시작 후 적용된다.</div>
|
|
5018
|
+
|
|
5019
|
+
<div class="form-line">
|
|
5020
|
+
<div class="header">
|
|
5021
|
+
<div class="title">루트 프로필 사용</div>
|
|
5022
|
+
<div class="description">켜고 작업 폴더를 채우면 해당 프로필을 만들어 기본 프로필로 삼는다.</div>
|
|
5023
|
+
</div>
|
|
5024
|
+
<toggle [(ngModel)]="config.store.agentDeck.rootProfile" (ngModelChange)="config.save()"></toggle>
|
|
5025
|
+
</div>
|
|
5026
|
+
|
|
5027
|
+
<ng-container *ngIf="config.store.agentDeck.rootProfile">
|
|
5028
|
+
<div class="form-line">
|
|
5029
|
+
<div class="header">
|
|
5030
|
+
<div class="title">프로필 이름</div>
|
|
5031
|
+
</div>
|
|
5032
|
+
<input class="form-control w-50" type="text"
|
|
5033
|
+
[(ngModel)]="config.store.agentDeck.rootProfileName" (change)="config.save()">
|
|
5034
|
+
</div>
|
|
5035
|
+
|
|
5036
|
+
<div class="form-line">
|
|
5037
|
+
<div class="header">
|
|
5038
|
+
<div class="title">작업 폴더</div>
|
|
5039
|
+
<div class="description">비우면 프로필을 만들지 않는다. 직접 적을 때는 백슬래시 이스케이프 함정을 피하려고 슬래시로 적는다 (예: D:/Project).</div>
|
|
5040
|
+
</div>
|
|
5041
|
+
<div class="input-group w-50">
|
|
5042
|
+
<input class="form-control" type="text" placeholder="D:/Project"
|
|
5043
|
+
[(ngModel)]="config.store.agentDeck.rootProfileCwd" (change)="config.save()">
|
|
5044
|
+
<button class="btn btn-secondary" type="button" (click)="pickCwd()">
|
|
5045
|
+
<i class="fas fa-folder-open me-2"></i>찾아보기
|
|
5046
|
+
</button>
|
|
5047
|
+
</div>
|
|
5048
|
+
</div>
|
|
5049
|
+
|
|
5050
|
+
<div class="form-line">
|
|
5051
|
+
<div class="header">
|
|
5052
|
+
<div class="title">실행 명령</div>
|
|
5053
|
+
</div>
|
|
5054
|
+
<input class="form-control w-50" type="text"
|
|
5055
|
+
[(ngModel)]="config.store.agentDeck.rootProfileCommand" (change)="config.save()">
|
|
5056
|
+
</div>
|
|
5057
|
+
|
|
5058
|
+
</ng-container>
|
|
5059
|
+
|
|
5060
|
+
<div class="text-muted mt-4">
|
|
5061
|
+
화면비·사이드바 폭·투명도·상태 감지·입력 처리 등 나머지 값은 기본값으로 고정돼 있다.
|
|
5062
|
+
바꿔야 한다면 설정 파일의 <code>agentDeck.*</code> 를 직접 고친다.
|
|
5063
|
+
단축키는 <b>단축키</b> 탭의 <code>agentdeck-*</code> 항목에서 바꾼다.
|
|
5064
|
+
</div>
|
|
5065
|
+
`,
|
|
5066
|
+
}),
|
|
5067
|
+
__metadata("design:paramtypes", [tabby_core__WEBPACK_IMPORTED_MODULE_1__.ConfigService,
|
|
5068
|
+
tabby_core__WEBPACK_IMPORTED_MODULE_1__.PlatformService])
|
|
5069
|
+
], AgentDeckSettingsTabComponent);
|
|
5070
|
+
|
|
5071
|
+
|
|
5072
|
+
|
|
5073
|
+
/***/ },
|
|
5074
|
+
|
|
5075
|
+
/***/ "./src/settings.ts"
|
|
5076
|
+
/*!*************************!*\
|
|
5077
|
+
!*** ./src/settings.ts ***!
|
|
5078
|
+
\*************************/
|
|
5079
|
+
(__unused_webpack_module, __webpack_exports__, __webpack_require__) {
|
|
5080
|
+
|
|
5081
|
+
__webpack_require__.r(__webpack_exports__);
|
|
5082
|
+
/* harmony export */ __webpack_require__.d(__webpack_exports__, {
|
|
5083
|
+
/* harmony export */ AgentDeckSettingsTabProvider: () => (/* binding */ AgentDeckSettingsTabProvider)
|
|
5084
|
+
/* harmony export */ });
|
|
5085
|
+
/* harmony import */ var _angular_core__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! @angular/core */ "@angular/core");
|
|
5086
|
+
/* harmony import */ var _angular_core__WEBPACK_IMPORTED_MODULE_0___default = /*#__PURE__*/__webpack_require__.n(_angular_core__WEBPACK_IMPORTED_MODULE_0__);
|
|
5087
|
+
/* harmony import */ var tabby_settings__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(/*! tabby-settings */ "tabby-settings");
|
|
5088
|
+
/* harmony import */ var tabby_settings__WEBPACK_IMPORTED_MODULE_1___default = /*#__PURE__*/__webpack_require__.n(tabby_settings__WEBPACK_IMPORTED_MODULE_1__);
|
|
5089
|
+
/* harmony import */ var _settings_component__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(/*! ./settings.component */ "./src/settings.component.ts");
|
|
5090
|
+
var __decorate = (undefined && undefined.__decorate) || function (decorators, target, key, desc) {
|
|
5091
|
+
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
5092
|
+
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
|
5093
|
+
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;
|
|
5094
|
+
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
5095
|
+
};
|
|
5096
|
+
|
|
5097
|
+
|
|
5098
|
+
|
|
5099
|
+
/**
|
|
5100
|
+
* 설정 창 왼쪽 목록에 AgentDeck 탭을 끼워 넣는다.
|
|
5101
|
+
* `weight` 를 주지 않으면 등록 순서 뒤쪽에 붙는다 — 순정 탭들 아래로 가도록 그대로 둔다.
|
|
5102
|
+
*/
|
|
5103
|
+
let AgentDeckSettingsTabProvider = class AgentDeckSettingsTabProvider extends tabby_settings__WEBPACK_IMPORTED_MODULE_1__.SettingsTabProvider {
|
|
5104
|
+
constructor() {
|
|
5105
|
+
super(...arguments);
|
|
5106
|
+
this.id = 'agentdeck';
|
|
5107
|
+
this.icon = 'layer-group';
|
|
5108
|
+
this.title = 'AgentDeck';
|
|
5109
|
+
}
|
|
5110
|
+
getComponentType() {
|
|
5111
|
+
return _settings_component__WEBPACK_IMPORTED_MODULE_2__.AgentDeckSettingsTabComponent;
|
|
5112
|
+
}
|
|
5113
|
+
};
|
|
5114
|
+
AgentDeckSettingsTabProvider = __decorate([
|
|
5115
|
+
(0,_angular_core__WEBPACK_IMPORTED_MODULE_0__.Injectable)()
|
|
5116
|
+
], AgentDeckSettingsTabProvider);
|
|
5117
|
+
|
|
5118
|
+
|
|
5119
|
+
|
|
3164
5120
|
/***/ },
|
|
3165
5121
|
|
|
3166
5122
|
/***/ "./src/status.service.ts"
|
|
@@ -3206,7 +5162,10 @@ let WorkStatusService = class WorkStatusService {
|
|
|
3206
5162
|
get(tab) {
|
|
3207
5163
|
let s = this.states.get(tab);
|
|
3208
5164
|
if (!s) {
|
|
3209
|
-
s = {
|
|
5165
|
+
s = {
|
|
5166
|
+
status: 'idle', label: '', since: Date.now(), pinned: false,
|
|
5167
|
+
lastOutput: 0, busyMode: false, lastBusy: 0,
|
|
5168
|
+
};
|
|
3210
5169
|
this.states.set(tab, s);
|
|
3211
5170
|
}
|
|
3212
5171
|
return s;
|
|
@@ -3247,6 +5206,21 @@ let WorkStatusService = class WorkStatusService {
|
|
|
3247
5206
|
markOutput(tab) {
|
|
3248
5207
|
this.get(tab).lastOutput = Date.now();
|
|
3249
5208
|
}
|
|
5209
|
+
/**
|
|
5210
|
+
* 에이전트가 "작업 중" 이라고 스스로 밝힌 순간 — 스피너나 "esc to interrupt" 같은 표시.
|
|
5211
|
+
*
|
|
5212
|
+
* 이 신호를 한 번이라도 본 탭은 그 뒤로 신호만 보고 판단한다(busyMode).
|
|
5213
|
+
* 상태줄을 매초 다시 그리는 TUI 를 "계속 일하는 중" 으로 오해하지 않기 위해서다.
|
|
5214
|
+
*/
|
|
5215
|
+
markBusy(tab) {
|
|
5216
|
+
const s = this.get(tab);
|
|
5217
|
+
s.busyMode = true;
|
|
5218
|
+
s.lastBusy = Date.now();
|
|
5219
|
+
}
|
|
5220
|
+
/** 이 탭을 에이전트 TUI 로 볼지 — 아니면 예전처럼 출력 유무로 판단한다 */
|
|
5221
|
+
isBusyMode(tab) {
|
|
5222
|
+
return this.get(tab).busyMode;
|
|
5223
|
+
}
|
|
3250
5224
|
forget(tab) {
|
|
3251
5225
|
this.states.delete(tab);
|
|
3252
5226
|
this.changed.next();
|
|
@@ -3257,7 +5231,10 @@ let WorkStatusService = class WorkStatusService {
|
|
|
3257
5231
|
if (s.pinned || s.status !== 'running') {
|
|
3258
5232
|
return false;
|
|
3259
5233
|
}
|
|
3260
|
-
|
|
5234
|
+
// 에이전트 탭은 "작업 중" 신호가 끊긴 시점부터 잰다.
|
|
5235
|
+
// 일반 셸은 예전대로 출력이 끊긴 시점부터 잰다 (신호를 낼 줄 모르므로)
|
|
5236
|
+
const from = s.busyMode ? s.lastBusy : s.lastOutput;
|
|
5237
|
+
if (from && Date.now() - from > idleAfterMs) {
|
|
3261
5238
|
s.status = 'idle';
|
|
3262
5239
|
s.since = Date.now();
|
|
3263
5240
|
return true;
|
|
@@ -3303,6 +5280,16 @@ module.exports = __WEBPACK_EXTERNAL_MODULE__angular_core__;
|
|
|
3303
5280
|
|
|
3304
5281
|
/***/ },
|
|
3305
5282
|
|
|
5283
|
+
/***/ "@angular/forms"
|
|
5284
|
+
/*!*********************************!*\
|
|
5285
|
+
!*** external "@angular/forms" ***!
|
|
5286
|
+
\*********************************/
|
|
5287
|
+
(module) {
|
|
5288
|
+
|
|
5289
|
+
module.exports = __WEBPACK_EXTERNAL_MODULE__angular_forms__;
|
|
5290
|
+
|
|
5291
|
+
/***/ },
|
|
5292
|
+
|
|
3306
5293
|
/***/ "electron"
|
|
3307
5294
|
/*!***************************!*\
|
|
3308
5295
|
!*** external "electron" ***!
|
|
@@ -3460,25 +5447,31 @@ let __webpack_exports__ = {};
|
|
|
3460
5447
|
\**********************/
|
|
3461
5448
|
__webpack_require__.r(__webpack_exports__);
|
|
3462
5449
|
/* harmony export */ __webpack_require__.d(__webpack_exports__, {
|
|
3463
|
-
/* harmony export */ STATUS_STYLES: () => (/* reexport safe */
|
|
5450
|
+
/* harmony export */ STATUS_STYLES: () => (/* reexport safe */ _api__WEBPACK_IMPORTED_MODULE_15__.STATUS_STYLES),
|
|
3464
5451
|
/* harmony export */ "default": () => (__WEBPACK_DEFAULT_EXPORT__)
|
|
3465
5452
|
/* harmony export */ });
|
|
3466
5453
|
/* harmony import */ var _angular_core__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! @angular/core */ "@angular/core");
|
|
3467
5454
|
/* harmony import */ var _angular_core__WEBPACK_IMPORTED_MODULE_0___default = /*#__PURE__*/__webpack_require__.n(_angular_core__WEBPACK_IMPORTED_MODULE_0__);
|
|
3468
5455
|
/* harmony import */ var _angular_common__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(/*! @angular/common */ "@angular/common");
|
|
3469
5456
|
/* harmony import */ var _angular_common__WEBPACK_IMPORTED_MODULE_1___default = /*#__PURE__*/__webpack_require__.n(_angular_common__WEBPACK_IMPORTED_MODULE_1__);
|
|
3470
|
-
/* harmony import */ var
|
|
3471
|
-
/* harmony import */ var
|
|
3472
|
-
/* harmony import */ var
|
|
3473
|
-
/* harmony import */ var
|
|
3474
|
-
/* harmony import */ var
|
|
3475
|
-
/* harmony import */ var
|
|
3476
|
-
/* harmony import */ var
|
|
3477
|
-
/* harmony import */ var
|
|
3478
|
-
/* harmony import */ var
|
|
3479
|
-
/* harmony import */ var
|
|
3480
|
-
/* harmony import */ var
|
|
3481
|
-
/* harmony import */ var
|
|
5457
|
+
/* harmony import */ var _angular_forms__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(/*! @angular/forms */ "@angular/forms");
|
|
5458
|
+
/* harmony import */ var _angular_forms__WEBPACK_IMPORTED_MODULE_2___default = /*#__PURE__*/__webpack_require__.n(_angular_forms__WEBPACK_IMPORTED_MODULE_2__);
|
|
5459
|
+
/* harmony import */ var tabby_core__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(/*! tabby-core */ "tabby-core");
|
|
5460
|
+
/* harmony import */ var tabby_core__WEBPACK_IMPORTED_MODULE_3___default = /*#__PURE__*/__webpack_require__.n(tabby_core__WEBPACK_IMPORTED_MODULE_3__);
|
|
5461
|
+
/* harmony import */ var tabby_settings__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(/*! tabby-settings */ "tabby-settings");
|
|
5462
|
+
/* harmony import */ var tabby_settings__WEBPACK_IMPORTED_MODULE_4___default = /*#__PURE__*/__webpack_require__.n(tabby_settings__WEBPACK_IMPORTED_MODULE_4__);
|
|
5463
|
+
/* harmony import */ var tabby_terminal__WEBPACK_IMPORTED_MODULE_5__ = __webpack_require__(/*! tabby-terminal */ "tabby-terminal");
|
|
5464
|
+
/* harmony import */ var tabby_terminal__WEBPACK_IMPORTED_MODULE_5___default = /*#__PURE__*/__webpack_require__.n(tabby_terminal__WEBPACK_IMPORTED_MODULE_5__);
|
|
5465
|
+
/* harmony import */ var _config__WEBPACK_IMPORTED_MODULE_6__ = __webpack_require__(/*! ./config */ "./src/config.ts");
|
|
5466
|
+
/* harmony import */ var _hotkeys__WEBPACK_IMPORTED_MODULE_7__ = __webpack_require__(/*! ./hotkeys */ "./src/hotkeys.ts");
|
|
5467
|
+
/* harmony import */ var _decorator__WEBPACK_IMPORTED_MODULE_8__ = __webpack_require__(/*! ./decorator */ "./src/decorator.ts");
|
|
5468
|
+
/* harmony import */ var _deck_service__WEBPACK_IMPORTED_MODULE_9__ = __webpack_require__(/*! ./deck.service */ "./src/deck.service.ts");
|
|
5469
|
+
/* harmony import */ var _profile_service__WEBPACK_IMPORTED_MODULE_10__ = __webpack_require__(/*! ./profile.service */ "./src/profile.service.ts");
|
|
5470
|
+
/* harmony import */ var _notify_service__WEBPACK_IMPORTED_MODULE_11__ = __webpack_require__(/*! ./notify.service */ "./src/notify.service.ts");
|
|
5471
|
+
/* harmony import */ var _settings__WEBPACK_IMPORTED_MODULE_12__ = __webpack_require__(/*! ./settings */ "./src/settings.ts");
|
|
5472
|
+
/* harmony import */ var _settings_component__WEBPACK_IMPORTED_MODULE_13__ = __webpack_require__(/*! ./settings.component */ "./src/settings.component.ts");
|
|
5473
|
+
/* harmony import */ var _styles_scss__WEBPACK_IMPORTED_MODULE_14__ = __webpack_require__(/*! ./styles.scss */ "./src/styles.scss");
|
|
5474
|
+
/* harmony import */ var _api__WEBPACK_IMPORTED_MODULE_15__ = __webpack_require__(/*! ./api */ "./src/api.ts");
|
|
3482
5475
|
var __decorate = (undefined && undefined.__decorate) || function (decorators, target, key, desc) {
|
|
3483
5476
|
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
3484
5477
|
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
|
@@ -3500,6 +5493,10 @@ var __metadata = (undefined && undefined.__metadata) || function (k, v) {
|
|
|
3500
5493
|
|
|
3501
5494
|
|
|
3502
5495
|
|
|
5496
|
+
|
|
5497
|
+
|
|
5498
|
+
|
|
5499
|
+
|
|
3503
5500
|
let AgentDeckModule = class AgentDeckModule {
|
|
3504
5501
|
constructor(deck, profiles, notify) {
|
|
3505
5502
|
// 세 서비스 모두 app.ready$ 를 기다렸다가 스스로 붙는다
|
|
@@ -3512,17 +5509,23 @@ AgentDeckModule = __decorate([
|
|
|
3512
5509
|
(0,_angular_core__WEBPACK_IMPORTED_MODULE_0__.NgModule)({
|
|
3513
5510
|
imports: [
|
|
3514
5511
|
_angular_common__WEBPACK_IMPORTED_MODULE_1__.CommonModule,
|
|
3515
|
-
(
|
|
5512
|
+
// 설정 탭이 [(ngModel)] 로 config.store 를 직접 물고 있어서 필요하다
|
|
5513
|
+
_angular_forms__WEBPACK_IMPORTED_MODULE_2__.FormsModule,
|
|
5514
|
+
(tabby_core__WEBPACK_IMPORTED_MODULE_3___default()),
|
|
3516
5515
|
],
|
|
3517
5516
|
providers: [
|
|
3518
|
-
{ provide:
|
|
3519
|
-
{ provide:
|
|
3520
|
-
{ provide:
|
|
5517
|
+
{ provide: tabby_core__WEBPACK_IMPORTED_MODULE_3__.ConfigProvider, useClass: _config__WEBPACK_IMPORTED_MODULE_6__.AgentDeckConfigProvider, multi: true },
|
|
5518
|
+
{ provide: tabby_core__WEBPACK_IMPORTED_MODULE_3__.HotkeyProvider, useClass: _hotkeys__WEBPACK_IMPORTED_MODULE_7__.AgentDeckHotkeyProvider, multi: true },
|
|
5519
|
+
{ provide: tabby_terminal__WEBPACK_IMPORTED_MODULE_5__.TerminalDecorator, useClass: _decorator__WEBPACK_IMPORTED_MODULE_8__.AgentDeckDecorator, multi: true },
|
|
5520
|
+
{ provide: tabby_settings__WEBPACK_IMPORTED_MODULE_4__.SettingsTabProvider, useClass: _settings__WEBPACK_IMPORTED_MODULE_12__.AgentDeckSettingsTabProvider, multi: true },
|
|
5521
|
+
],
|
|
5522
|
+
declarations: [
|
|
5523
|
+
_settings_component__WEBPACK_IMPORTED_MODULE_13__.AgentDeckSettingsTabComponent,
|
|
3521
5524
|
],
|
|
3522
5525
|
}),
|
|
3523
|
-
__metadata("design:paramtypes", [
|
|
3524
|
-
|
|
3525
|
-
|
|
5526
|
+
__metadata("design:paramtypes", [_deck_service__WEBPACK_IMPORTED_MODULE_9__.AgentDeckService,
|
|
5527
|
+
_profile_service__WEBPACK_IMPORTED_MODULE_10__.AgentDeckProfileService,
|
|
5528
|
+
_notify_service__WEBPACK_IMPORTED_MODULE_11__.WorkNotifyService])
|
|
3526
5529
|
], AgentDeckModule);
|
|
3527
5530
|
/* harmony default export */ const __WEBPACK_DEFAULT_EXPORT__ = (AgentDeckModule);
|
|
3528
5531
|
|