tabby-agentdeck 0.1.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/LICENSE +21 -0
- package/README.md +240 -0
- package/dist/api.d.ts +21 -0
- package/dist/config.d.ts +179 -0
- package/dist/deck.service.d.ts +331 -0
- package/dist/decorator.d.ts +28 -0
- package/dist/detect.d.ts +15 -0
- package/dist/hotkeys.d.ts +12 -0
- package/dist/index.d.ts +8 -0
- package/dist/index.js +3535 -0
- package/dist/index.js.map +1 -0
- package/dist/notify.service.d.ts +89 -0
- package/dist/profile.service.d.ts +16 -0
- package/dist/prompt.d.ts +26 -0
- package/dist/status.service.d.ts +32 -0
- package/hooks/agentdeck-label.js +53 -0
- package/hooks/agentdeck-notify.ps1 +159 -0
- package/hooks/agentdeck-status.ps1 +33 -0
- package/install.ps1 +82 -0
- package/package.json +50 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,3535 @@
|
|
|
1
|
+
(function webpackUniversalModuleDefinition(root, factory) {
|
|
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"));
|
|
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);
|
|
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"]);
|
|
8
|
+
for(var i in a) (typeof exports === 'object' ? exports : root)[i] = a[i];
|
|
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__) => {
|
|
11
|
+
return /******/ (() => { // webpackBootstrap
|
|
12
|
+
/******/ "use strict";
|
|
13
|
+
/******/ var __webpack_modules__ = ({
|
|
14
|
+
|
|
15
|
+
/***/ "./node_modules/css-loader/dist/cjs.js!./node_modules/sass-loader/dist/cjs.js!./src/styles.scss"
|
|
16
|
+
/*!******************************************************************************************************!*\
|
|
17
|
+
!*** ./node_modules/css-loader/dist/cjs.js!./node_modules/sass-loader/dist/cjs.js!./src/styles.scss ***!
|
|
18
|
+
\******************************************************************************************************/
|
|
19
|
+
(module, __webpack_exports__, __webpack_require__) {
|
|
20
|
+
|
|
21
|
+
__webpack_require__.r(__webpack_exports__);
|
|
22
|
+
/* harmony export */ __webpack_require__.d(__webpack_exports__, {
|
|
23
|
+
/* harmony export */ "default": () => (__WEBPACK_DEFAULT_EXPORT__)
|
|
24
|
+
/* harmony export */ });
|
|
25
|
+
/* harmony import */ var _node_modules_css_loader_dist_runtime_sourceMaps_js__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! ../node_modules/css-loader/dist/runtime/sourceMaps.js */ "./node_modules/css-loader/dist/runtime/sourceMaps.js");
|
|
26
|
+
/* harmony import */ var _node_modules_css_loader_dist_runtime_sourceMaps_js__WEBPACK_IMPORTED_MODULE_0___default = /*#__PURE__*/__webpack_require__.n(_node_modules_css_loader_dist_runtime_sourceMaps_js__WEBPACK_IMPORTED_MODULE_0__);
|
|
27
|
+
/* harmony import */ var _node_modules_css_loader_dist_runtime_api_js__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(/*! ../node_modules/css-loader/dist/runtime/api.js */ "./node_modules/css-loader/dist/runtime/api.js");
|
|
28
|
+
/* harmony import */ var _node_modules_css_loader_dist_runtime_api_js__WEBPACK_IMPORTED_MODULE_1___default = /*#__PURE__*/__webpack_require__.n(_node_modules_css_loader_dist_runtime_api_js__WEBPACK_IMPORTED_MODULE_1__);
|
|
29
|
+
// Imports
|
|
30
|
+
|
|
31
|
+
|
|
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
|
+
// Module
|
|
34
|
+
___CSS_LOADER_EXPORT___.push([module.id, `#agentdeck-sidebar {
|
|
35
|
+
display: flex;
|
|
36
|
+
flex-direction: column;
|
|
37
|
+
flex: 0 0 auto;
|
|
38
|
+
height: 100%;
|
|
39
|
+
min-width: 180px;
|
|
40
|
+
overflow: hidden;
|
|
41
|
+
background: var(--ad-panel-bg, var(--bs-body-bg, #1e2127));
|
|
42
|
+
border-left: 1px solid rgba(255, 255, 255, 0.08);
|
|
43
|
+
font-size: 12px;
|
|
44
|
+
user-select: none;
|
|
45
|
+
-webkit-app-region: no-drag;
|
|
46
|
+
}
|
|
47
|
+
#agentdeck-sidebar .ad-head {
|
|
48
|
+
display: flex;
|
|
49
|
+
align-items: center;
|
|
50
|
+
justify-content: space-between;
|
|
51
|
+
padding: 10px 12px 8px;
|
|
52
|
+
letter-spacing: 0.12em;
|
|
53
|
+
font-size: 10px;
|
|
54
|
+
font-weight: 700;
|
|
55
|
+
color: rgba(255, 255, 255, 0.4);
|
|
56
|
+
-webkit-app-region: drag;
|
|
57
|
+
}
|
|
58
|
+
#agentdeck-sidebar .ad-head-count {
|
|
59
|
+
-webkit-app-region: no-drag;
|
|
60
|
+
padding: 1px 6px;
|
|
61
|
+
border-radius: 8px;
|
|
62
|
+
background: rgba(255, 255, 255, 0.08);
|
|
63
|
+
color: rgba(255, 255, 255, 0.55);
|
|
64
|
+
}
|
|
65
|
+
#agentdeck-sidebar .ad-list {
|
|
66
|
+
flex: 1 1 auto;
|
|
67
|
+
overflow-y: auto;
|
|
68
|
+
overflow-x: hidden;
|
|
69
|
+
padding: 2px 6px 8px;
|
|
70
|
+
}
|
|
71
|
+
#agentdeck-sidebar .ad-list::-webkit-scrollbar {
|
|
72
|
+
width: 8px;
|
|
73
|
+
}
|
|
74
|
+
#agentdeck-sidebar .ad-list::-webkit-scrollbar-thumb {
|
|
75
|
+
background: rgba(255, 255, 255, 0.14);
|
|
76
|
+
border-radius: 4px;
|
|
77
|
+
}
|
|
78
|
+
#agentdeck-sidebar .ad-list::-webkit-scrollbar-thumb:hover {
|
|
79
|
+
background: rgba(255, 255, 255, 0.24);
|
|
80
|
+
}
|
|
81
|
+
#agentdeck-sidebar .ad-tab {
|
|
82
|
+
position: relative;
|
|
83
|
+
display: flex;
|
|
84
|
+
align-items: stretch;
|
|
85
|
+
gap: 8px;
|
|
86
|
+
margin-bottom: 3px;
|
|
87
|
+
padding: 7px 6px 7px 0;
|
|
88
|
+
border-radius: 5px;
|
|
89
|
+
cursor: pointer;
|
|
90
|
+
transition: background 0.1s ease;
|
|
91
|
+
}
|
|
92
|
+
#agentdeck-sidebar .ad-tab:hover {
|
|
93
|
+
background: rgba(255, 255, 255, 0.05);
|
|
94
|
+
}
|
|
95
|
+
#agentdeck-sidebar .ad-tab:hover .ad-close {
|
|
96
|
+
opacity: 0.65;
|
|
97
|
+
}
|
|
98
|
+
#agentdeck-sidebar .ad-tab.active {
|
|
99
|
+
background: rgba(255, 255, 255, 0.1);
|
|
100
|
+
}
|
|
101
|
+
#agentdeck-sidebar .ad-tab.active .ad-title {
|
|
102
|
+
color: #fff;
|
|
103
|
+
}
|
|
104
|
+
#agentdeck-sidebar .ad-bar {
|
|
105
|
+
flex: 0 0 3px;
|
|
106
|
+
border-radius: 2px;
|
|
107
|
+
background: var(--ad-color, #6b727d);
|
|
108
|
+
}
|
|
109
|
+
#agentdeck-sidebar .ad-body {
|
|
110
|
+
flex: 1 1 auto;
|
|
111
|
+
min-width: 0;
|
|
112
|
+
}
|
|
113
|
+
#agentdeck-sidebar .ad-title {
|
|
114
|
+
overflow: hidden;
|
|
115
|
+
text-overflow: ellipsis;
|
|
116
|
+
white-space: nowrap;
|
|
117
|
+
color: rgba(255, 255, 255, 0.82);
|
|
118
|
+
font-size: 12.5px;
|
|
119
|
+
line-height: 1.35;
|
|
120
|
+
}
|
|
121
|
+
#agentdeck-sidebar .ad-meta {
|
|
122
|
+
display: flex;
|
|
123
|
+
align-items: center;
|
|
124
|
+
gap: 6px;
|
|
125
|
+
margin-top: 2px;
|
|
126
|
+
font-size: 10.5px;
|
|
127
|
+
line-height: 1.4;
|
|
128
|
+
color: rgba(255, 255, 255, 0.45);
|
|
129
|
+
}
|
|
130
|
+
#agentdeck-sidebar .ad-badge {
|
|
131
|
+
flex: 0 0 auto;
|
|
132
|
+
color: var(--ad-color, #6b727d);
|
|
133
|
+
font-weight: 600;
|
|
134
|
+
}
|
|
135
|
+
#agentdeck-sidebar .ad-badge.pinned {
|
|
136
|
+
text-decoration: underline dotted;
|
|
137
|
+
text-underline-offset: 2px;
|
|
138
|
+
}
|
|
139
|
+
#agentdeck-sidebar .ad-label {
|
|
140
|
+
overflow: hidden;
|
|
141
|
+
text-overflow: ellipsis;
|
|
142
|
+
white-space: nowrap;
|
|
143
|
+
color: rgba(255, 255, 255, 0.6);
|
|
144
|
+
}
|
|
145
|
+
#agentdeck-sidebar .ad-elapsed {
|
|
146
|
+
flex: 0 0 auto;
|
|
147
|
+
margin-left: auto;
|
|
148
|
+
font-variant-numeric: tabular-nums;
|
|
149
|
+
opacity: 0.55;
|
|
150
|
+
}
|
|
151
|
+
#agentdeck-sidebar .ad-label-input {
|
|
152
|
+
flex: 1 1 auto;
|
|
153
|
+
min-width: 0;
|
|
154
|
+
padding: 0 4px;
|
|
155
|
+
border: 1px solid rgba(255, 255, 255, 0.25);
|
|
156
|
+
border-radius: 3px;
|
|
157
|
+
background: rgba(0, 0, 0, 0.35);
|
|
158
|
+
color: #fff;
|
|
159
|
+
font-size: 10.5px;
|
|
160
|
+
outline: none;
|
|
161
|
+
}
|
|
162
|
+
#agentdeck-sidebar .ad-label-input:focus {
|
|
163
|
+
border-color: var(--ad-color, #61afef);
|
|
164
|
+
}
|
|
165
|
+
#agentdeck-sidebar .ad-close {
|
|
166
|
+
flex: 0 0 auto;
|
|
167
|
+
align-self: center;
|
|
168
|
+
width: 18px;
|
|
169
|
+
height: 18px;
|
|
170
|
+
padding: 0;
|
|
171
|
+
border: 0;
|
|
172
|
+
border-radius: 3px;
|
|
173
|
+
opacity: 0;
|
|
174
|
+
background: transparent;
|
|
175
|
+
color: rgba(255, 255, 255, 0.8);
|
|
176
|
+
font-size: 15px;
|
|
177
|
+
line-height: 1;
|
|
178
|
+
cursor: pointer;
|
|
179
|
+
transition: opacity 0.1s ease, background 0.1s ease;
|
|
180
|
+
}
|
|
181
|
+
#agentdeck-sidebar .ad-close:hover {
|
|
182
|
+
opacity: 1 !important;
|
|
183
|
+
background: rgba(255, 80, 80, 0.35);
|
|
184
|
+
}
|
|
185
|
+
#agentdeck-sidebar .ad-foot {
|
|
186
|
+
display: flex;
|
|
187
|
+
gap: 6px;
|
|
188
|
+
padding: 8px;
|
|
189
|
+
border-top: 1px solid rgba(255, 255, 255, 0.07);
|
|
190
|
+
}
|
|
191
|
+
#agentdeck-sidebar .ad-btn {
|
|
192
|
+
flex: 1 1 auto;
|
|
193
|
+
padding: 5px 8px;
|
|
194
|
+
border: 1px solid rgba(255, 255, 255, 0.12);
|
|
195
|
+
border-radius: 4px;
|
|
196
|
+
background: transparent;
|
|
197
|
+
color: rgba(255, 255, 255, 0.6);
|
|
198
|
+
font-size: 11px;
|
|
199
|
+
cursor: pointer;
|
|
200
|
+
}
|
|
201
|
+
#agentdeck-sidebar .ad-btn:hover {
|
|
202
|
+
background: rgba(255, 255, 255, 0.08);
|
|
203
|
+
color: #fff;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
.ad-menu {
|
|
207
|
+
position: fixed;
|
|
208
|
+
z-index: 10000;
|
|
209
|
+
min-width: 130px;
|
|
210
|
+
padding: 4px;
|
|
211
|
+
border: 1px solid rgba(255, 255, 255, 0.12);
|
|
212
|
+
border-radius: 6px;
|
|
213
|
+
background: #262a31;
|
|
214
|
+
box-shadow: 0 6px 24px rgba(0, 0, 0, 0.5);
|
|
215
|
+
font-size: 12px;
|
|
216
|
+
user-select: none;
|
|
217
|
+
}
|
|
218
|
+
.ad-menu .ad-menu-item {
|
|
219
|
+
padding: 6px 10px;
|
|
220
|
+
border-radius: 4px;
|
|
221
|
+
color: rgba(255, 255, 255, 0.75);
|
|
222
|
+
cursor: pointer;
|
|
223
|
+
}
|
|
224
|
+
.ad-menu .ad-menu-item:hover {
|
|
225
|
+
background: rgba(255, 255, 255, 0.1);
|
|
226
|
+
color: #fff;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
body.agentdeck-active .content.main > .tab-bar {
|
|
230
|
+
display: none !important;
|
|
231
|
+
}
|
|
232
|
+
body.agentdeck-active .window {
|
|
233
|
+
display: flex;
|
|
234
|
+
flex-direction: row;
|
|
235
|
+
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":""}]);
|
|
237
|
+
// Exports
|
|
238
|
+
/* harmony default export */ const __WEBPACK_DEFAULT_EXPORT__ = (___CSS_LOADER_EXPORT___);
|
|
239
|
+
|
|
240
|
+
|
|
241
|
+
/***/ },
|
|
242
|
+
|
|
243
|
+
/***/ "./node_modules/css-loader/dist/runtime/api.js"
|
|
244
|
+
/*!*****************************************************!*\
|
|
245
|
+
!*** ./node_modules/css-loader/dist/runtime/api.js ***!
|
|
246
|
+
\*****************************************************/
|
|
247
|
+
(module) {
|
|
248
|
+
|
|
249
|
+
|
|
250
|
+
|
|
251
|
+
/*
|
|
252
|
+
MIT License http://www.opensource.org/licenses/mit-license.php
|
|
253
|
+
Author Tobias Koppers @sokra
|
|
254
|
+
*/
|
|
255
|
+
module.exports = function (cssWithMappingToString) {
|
|
256
|
+
var list = [];
|
|
257
|
+
|
|
258
|
+
// return the list of modules as css string
|
|
259
|
+
list.toString = function toString() {
|
|
260
|
+
return this.map(function (item) {
|
|
261
|
+
var content = "";
|
|
262
|
+
var needLayer = typeof item[5] !== "undefined";
|
|
263
|
+
if (item[4]) {
|
|
264
|
+
content += "@supports (".concat(item[4], ") {");
|
|
265
|
+
}
|
|
266
|
+
if (item[2]) {
|
|
267
|
+
content += "@media ".concat(item[2], " {");
|
|
268
|
+
}
|
|
269
|
+
if (needLayer) {
|
|
270
|
+
content += "@layer".concat(item[5].length > 0 ? " ".concat(item[5]) : "", " {");
|
|
271
|
+
}
|
|
272
|
+
content += cssWithMappingToString(item);
|
|
273
|
+
if (needLayer) {
|
|
274
|
+
content += "}";
|
|
275
|
+
}
|
|
276
|
+
if (item[2]) {
|
|
277
|
+
content += "}";
|
|
278
|
+
}
|
|
279
|
+
if (item[4]) {
|
|
280
|
+
content += "}";
|
|
281
|
+
}
|
|
282
|
+
return content;
|
|
283
|
+
}).join("");
|
|
284
|
+
};
|
|
285
|
+
|
|
286
|
+
// import a list of modules into the list
|
|
287
|
+
list.i = function i(modules, media, dedupe, supports, layer) {
|
|
288
|
+
if (typeof modules === "string") {
|
|
289
|
+
modules = [[null, modules, undefined]];
|
|
290
|
+
}
|
|
291
|
+
var alreadyImportedModules = {};
|
|
292
|
+
if (dedupe) {
|
|
293
|
+
for (var k = 0; k < this.length; k++) {
|
|
294
|
+
var id = this[k][0];
|
|
295
|
+
if (id != null) {
|
|
296
|
+
alreadyImportedModules[id] = true;
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
for (var _k = 0; _k < modules.length; _k++) {
|
|
301
|
+
var item = [].concat(modules[_k]);
|
|
302
|
+
if (dedupe && alreadyImportedModules[item[0]]) {
|
|
303
|
+
continue;
|
|
304
|
+
}
|
|
305
|
+
if (typeof layer !== "undefined") {
|
|
306
|
+
if (typeof item[5] === "undefined") {
|
|
307
|
+
item[5] = layer;
|
|
308
|
+
} else {
|
|
309
|
+
item[1] = "@layer".concat(item[5].length > 0 ? " ".concat(item[5]) : "", " {").concat(item[1], "}");
|
|
310
|
+
item[5] = layer;
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
if (media) {
|
|
314
|
+
if (!item[2]) {
|
|
315
|
+
item[2] = media;
|
|
316
|
+
} else {
|
|
317
|
+
item[1] = "@media ".concat(item[2], " {").concat(item[1], "}");
|
|
318
|
+
item[2] = media;
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
if (supports) {
|
|
322
|
+
if (!item[4]) {
|
|
323
|
+
item[4] = "".concat(supports);
|
|
324
|
+
} else {
|
|
325
|
+
item[1] = "@supports (".concat(item[4], ") {").concat(item[1], "}");
|
|
326
|
+
item[4] = supports;
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
list.push(item);
|
|
330
|
+
}
|
|
331
|
+
};
|
|
332
|
+
return list;
|
|
333
|
+
};
|
|
334
|
+
|
|
335
|
+
/***/ },
|
|
336
|
+
|
|
337
|
+
/***/ "./node_modules/css-loader/dist/runtime/sourceMaps.js"
|
|
338
|
+
/*!************************************************************!*\
|
|
339
|
+
!*** ./node_modules/css-loader/dist/runtime/sourceMaps.js ***!
|
|
340
|
+
\************************************************************/
|
|
341
|
+
(module) {
|
|
342
|
+
|
|
343
|
+
|
|
344
|
+
|
|
345
|
+
module.exports = function (item) {
|
|
346
|
+
var content = item[1];
|
|
347
|
+
var cssMapping = item[3];
|
|
348
|
+
if (!cssMapping) {
|
|
349
|
+
return content;
|
|
350
|
+
}
|
|
351
|
+
if (typeof btoa === "function") {
|
|
352
|
+
var base64 = btoa(unescape(encodeURIComponent(JSON.stringify(cssMapping))));
|
|
353
|
+
var data = "sourceMappingURL=data:application/json;charset=utf-8;base64,".concat(base64);
|
|
354
|
+
var sourceMapping = "/*# ".concat(data, " */");
|
|
355
|
+
return [content].concat([sourceMapping]).join("\n");
|
|
356
|
+
}
|
|
357
|
+
return [content].join("\n");
|
|
358
|
+
};
|
|
359
|
+
|
|
360
|
+
/***/ },
|
|
361
|
+
|
|
362
|
+
/***/ "./src/styles.scss"
|
|
363
|
+
/*!*************************!*\
|
|
364
|
+
!*** ./src/styles.scss ***!
|
|
365
|
+
\*************************/
|
|
366
|
+
(__unused_webpack_module, __webpack_exports__, __webpack_require__) {
|
|
367
|
+
|
|
368
|
+
__webpack_require__.r(__webpack_exports__);
|
|
369
|
+
/* harmony export */ __webpack_require__.d(__webpack_exports__, {
|
|
370
|
+
/* harmony export */ "default": () => (__WEBPACK_DEFAULT_EXPORT__)
|
|
371
|
+
/* harmony export */ });
|
|
372
|
+
/* harmony import */ var _node_modules_style_loader_dist_runtime_injectStylesIntoStyleTag_js__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! !../node_modules/style-loader/dist/runtime/injectStylesIntoStyleTag.js */ "./node_modules/style-loader/dist/runtime/injectStylesIntoStyleTag.js");
|
|
373
|
+
/* harmony import */ var _node_modules_style_loader_dist_runtime_injectStylesIntoStyleTag_js__WEBPACK_IMPORTED_MODULE_0___default = /*#__PURE__*/__webpack_require__.n(_node_modules_style_loader_dist_runtime_injectStylesIntoStyleTag_js__WEBPACK_IMPORTED_MODULE_0__);
|
|
374
|
+
/* harmony import */ var _node_modules_style_loader_dist_runtime_styleDomAPI_js__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(/*! !../node_modules/style-loader/dist/runtime/styleDomAPI.js */ "./node_modules/style-loader/dist/runtime/styleDomAPI.js");
|
|
375
|
+
/* harmony import */ var _node_modules_style_loader_dist_runtime_styleDomAPI_js__WEBPACK_IMPORTED_MODULE_1___default = /*#__PURE__*/__webpack_require__.n(_node_modules_style_loader_dist_runtime_styleDomAPI_js__WEBPACK_IMPORTED_MODULE_1__);
|
|
376
|
+
/* harmony import */ var _node_modules_style_loader_dist_runtime_insertBySelector_js__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(/*! !../node_modules/style-loader/dist/runtime/insertBySelector.js */ "./node_modules/style-loader/dist/runtime/insertBySelector.js");
|
|
377
|
+
/* harmony import */ var _node_modules_style_loader_dist_runtime_insertBySelector_js__WEBPACK_IMPORTED_MODULE_2___default = /*#__PURE__*/__webpack_require__.n(_node_modules_style_loader_dist_runtime_insertBySelector_js__WEBPACK_IMPORTED_MODULE_2__);
|
|
378
|
+
/* harmony import */ var _node_modules_style_loader_dist_runtime_setAttributesWithoutAttributes_js__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(/*! !../node_modules/style-loader/dist/runtime/setAttributesWithoutAttributes.js */ "./node_modules/style-loader/dist/runtime/setAttributesWithoutAttributes.js");
|
|
379
|
+
/* harmony import */ var _node_modules_style_loader_dist_runtime_setAttributesWithoutAttributes_js__WEBPACK_IMPORTED_MODULE_3___default = /*#__PURE__*/__webpack_require__.n(_node_modules_style_loader_dist_runtime_setAttributesWithoutAttributes_js__WEBPACK_IMPORTED_MODULE_3__);
|
|
380
|
+
/* harmony import */ var _node_modules_style_loader_dist_runtime_insertStyleElement_js__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(/*! !../node_modules/style-loader/dist/runtime/insertStyleElement.js */ "./node_modules/style-loader/dist/runtime/insertStyleElement.js");
|
|
381
|
+
/* harmony import */ var _node_modules_style_loader_dist_runtime_insertStyleElement_js__WEBPACK_IMPORTED_MODULE_4___default = /*#__PURE__*/__webpack_require__.n(_node_modules_style_loader_dist_runtime_insertStyleElement_js__WEBPACK_IMPORTED_MODULE_4__);
|
|
382
|
+
/* harmony import */ var _node_modules_style_loader_dist_runtime_styleTagTransform_js__WEBPACK_IMPORTED_MODULE_5__ = __webpack_require__(/*! !../node_modules/style-loader/dist/runtime/styleTagTransform.js */ "./node_modules/style-loader/dist/runtime/styleTagTransform.js");
|
|
383
|
+
/* harmony import */ var _node_modules_style_loader_dist_runtime_styleTagTransform_js__WEBPACK_IMPORTED_MODULE_5___default = /*#__PURE__*/__webpack_require__.n(_node_modules_style_loader_dist_runtime_styleTagTransform_js__WEBPACK_IMPORTED_MODULE_5__);
|
|
384
|
+
/* harmony import */ var _node_modules_css_loader_dist_cjs_js_node_modules_sass_loader_dist_cjs_js_styles_scss__WEBPACK_IMPORTED_MODULE_6__ = __webpack_require__(/*! !!../node_modules/css-loader/dist/cjs.js!../node_modules/sass-loader/dist/cjs.js!./styles.scss */ "./node_modules/css-loader/dist/cjs.js!./node_modules/sass-loader/dist/cjs.js!./src/styles.scss");
|
|
385
|
+
|
|
386
|
+
|
|
387
|
+
|
|
388
|
+
|
|
389
|
+
|
|
390
|
+
|
|
391
|
+
|
|
392
|
+
|
|
393
|
+
|
|
394
|
+
|
|
395
|
+
|
|
396
|
+
var options = {};
|
|
397
|
+
|
|
398
|
+
options.styleTagTransform = (_node_modules_style_loader_dist_runtime_styleTagTransform_js__WEBPACK_IMPORTED_MODULE_5___default());
|
|
399
|
+
options.setAttributes = (_node_modules_style_loader_dist_runtime_setAttributesWithoutAttributes_js__WEBPACK_IMPORTED_MODULE_3___default());
|
|
400
|
+
|
|
401
|
+
options.insert = _node_modules_style_loader_dist_runtime_insertBySelector_js__WEBPACK_IMPORTED_MODULE_2___default().bind(null, "head");
|
|
402
|
+
|
|
403
|
+
options.domAPI = (_node_modules_style_loader_dist_runtime_styleDomAPI_js__WEBPACK_IMPORTED_MODULE_1___default());
|
|
404
|
+
options.insertStyleElement = (_node_modules_style_loader_dist_runtime_insertStyleElement_js__WEBPACK_IMPORTED_MODULE_4___default());
|
|
405
|
+
|
|
406
|
+
var update = _node_modules_style_loader_dist_runtime_injectStylesIntoStyleTag_js__WEBPACK_IMPORTED_MODULE_0___default()(_node_modules_css_loader_dist_cjs_js_node_modules_sass_loader_dist_cjs_js_styles_scss__WEBPACK_IMPORTED_MODULE_6__["default"], options);
|
|
407
|
+
|
|
408
|
+
|
|
409
|
+
|
|
410
|
+
|
|
411
|
+
/* harmony default export */ const __WEBPACK_DEFAULT_EXPORT__ = (_node_modules_css_loader_dist_cjs_js_node_modules_sass_loader_dist_cjs_js_styles_scss__WEBPACK_IMPORTED_MODULE_6__["default"] && _node_modules_css_loader_dist_cjs_js_node_modules_sass_loader_dist_cjs_js_styles_scss__WEBPACK_IMPORTED_MODULE_6__["default"].locals ? _node_modules_css_loader_dist_cjs_js_node_modules_sass_loader_dist_cjs_js_styles_scss__WEBPACK_IMPORTED_MODULE_6__["default"].locals : undefined);
|
|
412
|
+
|
|
413
|
+
|
|
414
|
+
/***/ },
|
|
415
|
+
|
|
416
|
+
/***/ "./node_modules/style-loader/dist/runtime/injectStylesIntoStyleTag.js"
|
|
417
|
+
/*!****************************************************************************!*\
|
|
418
|
+
!*** ./node_modules/style-loader/dist/runtime/injectStylesIntoStyleTag.js ***!
|
|
419
|
+
\****************************************************************************/
|
|
420
|
+
(module) {
|
|
421
|
+
|
|
422
|
+
|
|
423
|
+
|
|
424
|
+
var stylesInDOM = [];
|
|
425
|
+
function getIndexByIdentifier(identifier) {
|
|
426
|
+
var result = -1;
|
|
427
|
+
for (var i = 0; i < stylesInDOM.length; i++) {
|
|
428
|
+
if (stylesInDOM[i].identifier === identifier) {
|
|
429
|
+
result = i;
|
|
430
|
+
break;
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
return result;
|
|
434
|
+
}
|
|
435
|
+
function modulesToDom(list, options) {
|
|
436
|
+
var idCountMap = {};
|
|
437
|
+
var identifiers = [];
|
|
438
|
+
for (var i = 0; i < list.length; i++) {
|
|
439
|
+
var item = list[i];
|
|
440
|
+
var id = options.base ? item[0] + options.base : item[0];
|
|
441
|
+
var count = idCountMap[id] || 0;
|
|
442
|
+
var identifier = "".concat(id, " ").concat(count);
|
|
443
|
+
idCountMap[id] = count + 1;
|
|
444
|
+
var indexByIdentifier = getIndexByIdentifier(identifier);
|
|
445
|
+
var obj = {
|
|
446
|
+
css: item[1],
|
|
447
|
+
media: item[2],
|
|
448
|
+
sourceMap: item[3],
|
|
449
|
+
supports: item[4],
|
|
450
|
+
layer: item[5]
|
|
451
|
+
};
|
|
452
|
+
if (indexByIdentifier !== -1) {
|
|
453
|
+
stylesInDOM[indexByIdentifier].references++;
|
|
454
|
+
stylesInDOM[indexByIdentifier].updater(obj);
|
|
455
|
+
} else {
|
|
456
|
+
var updater = addElementStyle(obj, options);
|
|
457
|
+
options.byIndex = i;
|
|
458
|
+
stylesInDOM.splice(i, 0, {
|
|
459
|
+
identifier: identifier,
|
|
460
|
+
updater: updater,
|
|
461
|
+
references: 1
|
|
462
|
+
});
|
|
463
|
+
}
|
|
464
|
+
identifiers.push(identifier);
|
|
465
|
+
}
|
|
466
|
+
return identifiers;
|
|
467
|
+
}
|
|
468
|
+
function addElementStyle(obj, options) {
|
|
469
|
+
var api = options.domAPI(options);
|
|
470
|
+
api.update(obj);
|
|
471
|
+
var updater = function updater(newObj) {
|
|
472
|
+
if (newObj) {
|
|
473
|
+
if (newObj.css === obj.css && newObj.media === obj.media && newObj.sourceMap === obj.sourceMap && newObj.supports === obj.supports && newObj.layer === obj.layer) {
|
|
474
|
+
return;
|
|
475
|
+
}
|
|
476
|
+
api.update(obj = newObj);
|
|
477
|
+
} else {
|
|
478
|
+
api.remove();
|
|
479
|
+
}
|
|
480
|
+
};
|
|
481
|
+
return updater;
|
|
482
|
+
}
|
|
483
|
+
module.exports = function (list, options) {
|
|
484
|
+
options = options || {};
|
|
485
|
+
list = list || [];
|
|
486
|
+
var lastIdentifiers = modulesToDom(list, options);
|
|
487
|
+
return function update(newList) {
|
|
488
|
+
newList = newList || [];
|
|
489
|
+
for (var i = 0; i < lastIdentifiers.length; i++) {
|
|
490
|
+
var identifier = lastIdentifiers[i];
|
|
491
|
+
var index = getIndexByIdentifier(identifier);
|
|
492
|
+
stylesInDOM[index].references--;
|
|
493
|
+
}
|
|
494
|
+
var newLastIdentifiers = modulesToDom(newList, options);
|
|
495
|
+
for (var _i = 0; _i < lastIdentifiers.length; _i++) {
|
|
496
|
+
var _identifier = lastIdentifiers[_i];
|
|
497
|
+
var _index = getIndexByIdentifier(_identifier);
|
|
498
|
+
if (stylesInDOM[_index].references === 0) {
|
|
499
|
+
stylesInDOM[_index].updater();
|
|
500
|
+
stylesInDOM.splice(_index, 1);
|
|
501
|
+
}
|
|
502
|
+
}
|
|
503
|
+
lastIdentifiers = newLastIdentifiers;
|
|
504
|
+
};
|
|
505
|
+
};
|
|
506
|
+
|
|
507
|
+
/***/ },
|
|
508
|
+
|
|
509
|
+
/***/ "./node_modules/style-loader/dist/runtime/insertBySelector.js"
|
|
510
|
+
/*!********************************************************************!*\
|
|
511
|
+
!*** ./node_modules/style-loader/dist/runtime/insertBySelector.js ***!
|
|
512
|
+
\********************************************************************/
|
|
513
|
+
(module) {
|
|
514
|
+
|
|
515
|
+
|
|
516
|
+
|
|
517
|
+
var memo = {};
|
|
518
|
+
|
|
519
|
+
/* istanbul ignore next */
|
|
520
|
+
function getTarget(target) {
|
|
521
|
+
if (typeof memo[target] === "undefined") {
|
|
522
|
+
var styleTarget = document.querySelector(target);
|
|
523
|
+
|
|
524
|
+
// Special case to return head of iframe instead of iframe itself
|
|
525
|
+
if (window.HTMLIFrameElement && styleTarget instanceof window.HTMLIFrameElement) {
|
|
526
|
+
try {
|
|
527
|
+
// This will throw an exception if access to iframe is blocked
|
|
528
|
+
// due to cross-origin restrictions
|
|
529
|
+
styleTarget = styleTarget.contentDocument.head;
|
|
530
|
+
} catch (e) {
|
|
531
|
+
// istanbul ignore next
|
|
532
|
+
styleTarget = null;
|
|
533
|
+
}
|
|
534
|
+
}
|
|
535
|
+
memo[target] = styleTarget;
|
|
536
|
+
}
|
|
537
|
+
return memo[target];
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
/* istanbul ignore next */
|
|
541
|
+
function insertBySelector(insert, style) {
|
|
542
|
+
var target = getTarget(insert);
|
|
543
|
+
if (!target) {
|
|
544
|
+
throw new Error("Couldn't find a style target. This probably means that the value for the 'insert' parameter is invalid.");
|
|
545
|
+
}
|
|
546
|
+
target.appendChild(style);
|
|
547
|
+
}
|
|
548
|
+
module.exports = insertBySelector;
|
|
549
|
+
|
|
550
|
+
/***/ },
|
|
551
|
+
|
|
552
|
+
/***/ "./node_modules/style-loader/dist/runtime/insertStyleElement.js"
|
|
553
|
+
/*!**********************************************************************!*\
|
|
554
|
+
!*** ./node_modules/style-loader/dist/runtime/insertStyleElement.js ***!
|
|
555
|
+
\**********************************************************************/
|
|
556
|
+
(module) {
|
|
557
|
+
|
|
558
|
+
|
|
559
|
+
|
|
560
|
+
/* istanbul ignore next */
|
|
561
|
+
function insertStyleElement(options) {
|
|
562
|
+
var element = document.createElement("style");
|
|
563
|
+
options.setAttributes(element, options.attributes);
|
|
564
|
+
options.insert(element, options.options);
|
|
565
|
+
return element;
|
|
566
|
+
}
|
|
567
|
+
module.exports = insertStyleElement;
|
|
568
|
+
|
|
569
|
+
/***/ },
|
|
570
|
+
|
|
571
|
+
/***/ "./node_modules/style-loader/dist/runtime/setAttributesWithoutAttributes.js"
|
|
572
|
+
/*!**********************************************************************************!*\
|
|
573
|
+
!*** ./node_modules/style-loader/dist/runtime/setAttributesWithoutAttributes.js ***!
|
|
574
|
+
\**********************************************************************************/
|
|
575
|
+
(module, __unused_webpack_exports, __webpack_require__) {
|
|
576
|
+
|
|
577
|
+
|
|
578
|
+
|
|
579
|
+
/* istanbul ignore next */
|
|
580
|
+
function setAttributesWithoutAttributes(styleElement) {
|
|
581
|
+
var nonce = true ? __webpack_require__.nc : 0;
|
|
582
|
+
if (nonce) {
|
|
583
|
+
styleElement.setAttribute("nonce", nonce);
|
|
584
|
+
}
|
|
585
|
+
}
|
|
586
|
+
module.exports = setAttributesWithoutAttributes;
|
|
587
|
+
|
|
588
|
+
/***/ },
|
|
589
|
+
|
|
590
|
+
/***/ "./node_modules/style-loader/dist/runtime/styleDomAPI.js"
|
|
591
|
+
/*!***************************************************************!*\
|
|
592
|
+
!*** ./node_modules/style-loader/dist/runtime/styleDomAPI.js ***!
|
|
593
|
+
\***************************************************************/
|
|
594
|
+
(module) {
|
|
595
|
+
|
|
596
|
+
|
|
597
|
+
|
|
598
|
+
/* istanbul ignore next */
|
|
599
|
+
function apply(styleElement, options, obj) {
|
|
600
|
+
var css = "";
|
|
601
|
+
if (obj.supports) {
|
|
602
|
+
css += "@supports (".concat(obj.supports, ") {");
|
|
603
|
+
}
|
|
604
|
+
if (obj.media) {
|
|
605
|
+
css += "@media ".concat(obj.media, " {");
|
|
606
|
+
}
|
|
607
|
+
var needLayer = typeof obj.layer !== "undefined";
|
|
608
|
+
if (needLayer) {
|
|
609
|
+
css += "@layer".concat(obj.layer.length > 0 ? " ".concat(obj.layer) : "", " {");
|
|
610
|
+
}
|
|
611
|
+
css += obj.css;
|
|
612
|
+
if (needLayer) {
|
|
613
|
+
css += "}";
|
|
614
|
+
}
|
|
615
|
+
if (obj.media) {
|
|
616
|
+
css += "}";
|
|
617
|
+
}
|
|
618
|
+
if (obj.supports) {
|
|
619
|
+
css += "}";
|
|
620
|
+
}
|
|
621
|
+
var sourceMap = obj.sourceMap;
|
|
622
|
+
if (sourceMap && typeof btoa !== "undefined") {
|
|
623
|
+
css += "\n/*# sourceMappingURL=data:application/json;base64,".concat(btoa(unescape(encodeURIComponent(JSON.stringify(sourceMap)))), " */");
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
// For old IE
|
|
627
|
+
/* istanbul ignore if */
|
|
628
|
+
options.styleTagTransform(css, styleElement, options.options);
|
|
629
|
+
}
|
|
630
|
+
function removeStyleElement(styleElement) {
|
|
631
|
+
// istanbul ignore if
|
|
632
|
+
if (styleElement.parentNode === null) {
|
|
633
|
+
return false;
|
|
634
|
+
}
|
|
635
|
+
styleElement.parentNode.removeChild(styleElement);
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
/* istanbul ignore next */
|
|
639
|
+
function domAPI(options) {
|
|
640
|
+
if (typeof document === "undefined") {
|
|
641
|
+
return {
|
|
642
|
+
update: function update() {},
|
|
643
|
+
remove: function remove() {}
|
|
644
|
+
};
|
|
645
|
+
}
|
|
646
|
+
var styleElement = options.insertStyleElement(options);
|
|
647
|
+
return {
|
|
648
|
+
update: function update(obj) {
|
|
649
|
+
apply(styleElement, options, obj);
|
|
650
|
+
},
|
|
651
|
+
remove: function remove() {
|
|
652
|
+
removeStyleElement(styleElement);
|
|
653
|
+
}
|
|
654
|
+
};
|
|
655
|
+
}
|
|
656
|
+
module.exports = domAPI;
|
|
657
|
+
|
|
658
|
+
/***/ },
|
|
659
|
+
|
|
660
|
+
/***/ "./node_modules/style-loader/dist/runtime/styleTagTransform.js"
|
|
661
|
+
/*!*********************************************************************!*\
|
|
662
|
+
!*** ./node_modules/style-loader/dist/runtime/styleTagTransform.js ***!
|
|
663
|
+
\*********************************************************************/
|
|
664
|
+
(module) {
|
|
665
|
+
|
|
666
|
+
|
|
667
|
+
|
|
668
|
+
/* istanbul ignore next */
|
|
669
|
+
function styleTagTransform(css, styleElement) {
|
|
670
|
+
if (styleElement.styleSheet) {
|
|
671
|
+
styleElement.styleSheet.cssText = css;
|
|
672
|
+
} else {
|
|
673
|
+
while (styleElement.firstChild) {
|
|
674
|
+
styleElement.removeChild(styleElement.firstChild);
|
|
675
|
+
}
|
|
676
|
+
styleElement.appendChild(document.createTextNode(css));
|
|
677
|
+
}
|
|
678
|
+
}
|
|
679
|
+
module.exports = styleTagTransform;
|
|
680
|
+
|
|
681
|
+
/***/ },
|
|
682
|
+
|
|
683
|
+
/***/ "./src/api.ts"
|
|
684
|
+
/*!********************!*\
|
|
685
|
+
!*** ./src/api.ts ***!
|
|
686
|
+
\********************/
|
|
687
|
+
(__unused_webpack_module, __webpack_exports__, __webpack_require__) {
|
|
688
|
+
|
|
689
|
+
__webpack_require__.r(__webpack_exports__);
|
|
690
|
+
/* harmony export */ __webpack_require__.d(__webpack_exports__, {
|
|
691
|
+
/* harmony export */ STATUS_STYLES: () => (/* binding */ STATUS_STYLES)
|
|
692
|
+
/* harmony export */ });
|
|
693
|
+
/** 상태별 표시 규칙 — 사이드바 배지와 좌측 컬러바에 함께 쓰인다 */
|
|
694
|
+
const STATUS_STYLES = {
|
|
695
|
+
running: { icon: '●', label: '진행중', color: '#e5c07b' },
|
|
696
|
+
waiting: { icon: '⏸', label: '승인대기', color: '#61afef' },
|
|
697
|
+
done: { icon: '✔', label: '완료', color: '#98c379' },
|
|
698
|
+
error: { icon: '✖', label: '오류', color: '#e06c75' },
|
|
699
|
+
idle: { icon: '○', label: '대기', color: '#6b727d' },
|
|
700
|
+
};
|
|
701
|
+
|
|
702
|
+
|
|
703
|
+
/***/ },
|
|
704
|
+
|
|
705
|
+
/***/ "./src/config.ts"
|
|
706
|
+
/*!***********************!*\
|
|
707
|
+
!*** ./src/config.ts ***!
|
|
708
|
+
\***********************/
|
|
709
|
+
(__unused_webpack_module, __webpack_exports__, __webpack_require__) {
|
|
710
|
+
|
|
711
|
+
__webpack_require__.r(__webpack_exports__);
|
|
712
|
+
/* harmony export */ __webpack_require__.d(__webpack_exports__, {
|
|
713
|
+
/* harmony export */ AgentDeckConfigProvider: () => (/* binding */ AgentDeckConfigProvider)
|
|
714
|
+
/* harmony export */ });
|
|
715
|
+
/* harmony import */ var tabby_core__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! tabby-core */ "tabby-core");
|
|
716
|
+
/* harmony import */ var tabby_core__WEBPACK_IMPORTED_MODULE_0___default = /*#__PURE__*/__webpack_require__.n(tabby_core__WEBPACK_IMPORTED_MODULE_0__);
|
|
717
|
+
|
|
718
|
+
/** @hidden */
|
|
719
|
+
class AgentDeckConfigProvider extends tabby_core__WEBPACK_IMPORTED_MODULE_0__.ConfigProvider {
|
|
720
|
+
constructor() {
|
|
721
|
+
super(...arguments);
|
|
722
|
+
this.defaults = {
|
|
723
|
+
agentDeck: {
|
|
724
|
+
/** 레이아웃/사이드바 전체 on-off */
|
|
725
|
+
enabled: true,
|
|
726
|
+
/** 터미널 뷰포트 가로:세로 비율 */
|
|
727
|
+
aspectW: 4,
|
|
728
|
+
aspectH: 3,
|
|
729
|
+
/**
|
|
730
|
+
* 사이드바 폭 제한 (px).
|
|
731
|
+
* sidebarMax 를 키우면 사이드바가 그만큼 넓어지고, 그래도 남는 폭은 터미널이 흡수한다
|
|
732
|
+
* (4:3 보다 "창을 꽉 채운다" 가 우선). config.yaml 의 agentDeck 아래에서 조절한다.
|
|
733
|
+
*/
|
|
734
|
+
sidebarMin: 200,
|
|
735
|
+
sidebarMax: 560,
|
|
736
|
+
/**
|
|
737
|
+
* 훅이 상태를 밀어 넣는 TCP 채널의 포트 (127.0.0.1 전용).
|
|
738
|
+
*
|
|
739
|
+
* Windows 임시 포트 범위(49152~65535) 아래로 잡는다 — 그 위 번호는 OS 가
|
|
740
|
+
* 다른 프로세스에 먼저 내줄 수 있어 기동 순서에 따라 충돌한다.
|
|
741
|
+
* 이 번호가 물려 있으면 0(임의 포트)으로 물러나고, 실제 번호는 어느 쪽이든
|
|
742
|
+
* `%LOCALAPPDATA%\tabby-agentdeck\port` 에 적히므로 훅은 그 파일만 보면 된다.
|
|
743
|
+
*/
|
|
744
|
+
notifyPort: 47500,
|
|
745
|
+
/**
|
|
746
|
+
* 외부 통보 채널(TCP + 상태 파일 폴링)을 열지.
|
|
747
|
+
*
|
|
748
|
+
* 기본은 꺼 둔다 — 라벨은 Enter 시점 입력창에서(`enterAsLabel`), 상태는 출력 패턴에서
|
|
749
|
+
* 플러그인이 스스로 얻으므로 바깥에서 밀어 넣어 줄 것이 없다. 켜 두면 쓰지도 않는
|
|
750
|
+
* 400ms 폴링과 포트 점유만 남는다. 외부 도구가 상태를 직접 보고하고 싶을 때만 켠다.
|
|
751
|
+
*/
|
|
752
|
+
notifyChannel: false,
|
|
753
|
+
/** PTY 출력이 이 시간(ms) 이상 없으면 running -> idle */
|
|
754
|
+
idleAfterMs: 2500,
|
|
755
|
+
/** 출력 패턴으로 상태를 자동 추론할지 */
|
|
756
|
+
autoDetect: true,
|
|
757
|
+
/** 사이드바에 경과 시간 표시 */
|
|
758
|
+
showElapsed: true,
|
|
759
|
+
/**
|
|
760
|
+
* 창이 가려져 있어도 사이드바를 계속 갱신한다 (Electron backgroundThrottling 해제).
|
|
761
|
+
*
|
|
762
|
+
* 끄면 Chromium 기본 동작으로 돌아간다 — 창이 hidden 이면 rAF 가 멈추고 타이머가
|
|
763
|
+
* 1초, 5분 이상 가려지면 분당 1회까지 느려져서 훅이 보낸 done 이 화면에 늦게 뜬다.
|
|
764
|
+
* 켜 두면 훅 보고가 바로 보이는 대신 가려진 동안에도 렌더 비용이 조금 든다
|
|
765
|
+
* (사이드바 DOM 갱신뿐이라 미미하다).
|
|
766
|
+
*/
|
|
767
|
+
keepRenderingWhenHidden: true,
|
|
768
|
+
/**
|
|
769
|
+
* 사이드바의 제목 줄에 탭 제목 대신 작업 라벨을 쓴다.
|
|
770
|
+
* Claude Code 는 세션 시작 때 붙인 콘솔 제목을 계속 들고 있어서 다음 작업으로
|
|
771
|
+
* 넘어가도 안 바뀐다(2026-08-28 실측) — 제목만 보여주면 사이드바가 옛 작업 이름에
|
|
772
|
+
* 박혀 있게 된다. 라벨이 없는 탭은 그대로 제목을 보여준다.
|
|
773
|
+
* Tabby 의 탭 제목 자체(customTitle)는 건드리지 않는다 — 앱이 넣는 제목을
|
|
774
|
+
* 가리지 않아야 그게 갱신되는지 계속 관측할 수 있다.
|
|
775
|
+
*/
|
|
776
|
+
labelAsTitle: true,
|
|
777
|
+
/**
|
|
778
|
+
* Enter 로 프롬프트를 보낼 때 그 원문을 작업 라벨로 삼는다 (claimEnterLabel 주석 참고).
|
|
779
|
+
* 훅도 콘솔 제목도 못 믿는 배포 환경에서 라벨을 최신으로 유지하는 유일한 경로다.
|
|
780
|
+
* Shift+Enter / Ctrl+Enter(줄바꿈)는 잡지 않는다.
|
|
781
|
+
*/
|
|
782
|
+
enterAsLabel: true,
|
|
783
|
+
/**
|
|
784
|
+
* Ctrl+V 를 agentdeck 이 직접 받는다 (document 캡처 단계에서 한 번만).
|
|
785
|
+
*
|
|
786
|
+
* 핫키(`agentdeck-paste`)로 받던 시절에는 Tabby 가 같은 keydown 을 두 경로로
|
|
787
|
+
* HotkeysService 에 밀어 넣어 한 번 눌러 두 번 붙는 일이 남아 있었다
|
|
788
|
+
* (deck.service.ts `ensurePasteHotkey` 주석에 원인). 캡처로 옮기고 전파를 끊으면서
|
|
789
|
+
* 경로가 하나로 정리됐다.
|
|
790
|
+
*
|
|
791
|
+
* false 로 두면 터미널 앱에 리터럴 Ctrl+V(0x16, quoted-insert)를 보낼 수 있다
|
|
792
|
+
*/
|
|
793
|
+
claimCtrlV: true,
|
|
794
|
+
/**
|
|
795
|
+
* 클립보드에 이미지만 있으면 Ctrl+V 로도 이미지가 들어가게 한다.
|
|
796
|
+
*
|
|
797
|
+
* 터미널 붙여넣기는 "클립보드 텍스트를 PTY 에 써넣는" 동작이라 이미지는 지나갈 수가 없다.
|
|
798
|
+
* 대신 에이전트 CLI 는 자기가 OS 클립보드를 직접 읽는 키를 따로 두므로,
|
|
799
|
+
* 이미지일 때만 그 키를 흘려보낸다 (어떤 키인지는 `imagePasteKey`).
|
|
800
|
+
*/
|
|
801
|
+
pasteImageWithCtrlV: true,
|
|
802
|
+
/**
|
|
803
|
+
* 이미지 전용 클립보드일 때 흘려보낼 키의 **기본값**. `'alt-v'`(ESC v) | `'ctrl-v'`(0x16).
|
|
804
|
+
*
|
|
805
|
+
* 평소에는 이 값이 쓰이지 않는다 — 탭에서 도는 앱을 보고 자동으로 갈라 보내기 때문이다
|
|
806
|
+
* (Claude Code = ESC v / Codex = 0x16, `deck.service.ts` `sendImagePasteKey` 주석에 실측).
|
|
807
|
+
* 앱을 못 알아낼 때(SSH 세션 등)의 폴백으로만 쓴다.
|
|
808
|
+
*
|
|
809
|
+
* Claude Code 는 PTY 로 들어온 0x16 에 반응하지 않는 것이 실측됐으므로
|
|
810
|
+
* (`.agentdeck-diag.log` 2026-08-28T09:33:47~48 에 `paste image (ctrl-v)` 5줄, 이미지 미삽입)
|
|
811
|
+
* 폴백은 더 널리 먹히는 ESC v 로 둔다.
|
|
812
|
+
*/
|
|
813
|
+
imagePasteKey: 'alt-v',
|
|
814
|
+
/**
|
|
815
|
+
* 우클릭(붙여넣기 / 선택 시 복사)을 agentdeck 이 직접 처리한다.
|
|
816
|
+
* Tabby 자체 처리를 끄고(terminal.rightClick = 'off') 캡처 단계에서 한 번만
|
|
817
|
+
* 처리하므로, 핸들러가 두 번 걸려 붙여넣기가 2중으로 들어가던 문제가 사라진다.
|
|
818
|
+
* false 로 두면 Tabby 순정 동작으로 되돌아간다
|
|
819
|
+
*/
|
|
820
|
+
claimRightClick: true,
|
|
821
|
+
/** 우클릭을 이 시간(ms) 이상 누르고 있으면 붙여넣기 대신 컨텍스트 메뉴 */
|
|
822
|
+
rightClickMenuMs: 250,
|
|
823
|
+
/**
|
|
824
|
+
* 터미널 배경 불투명도 (%). 100 이면 순정(완전 불투명).
|
|
825
|
+
*
|
|
826
|
+
* tabby-background 플러그인은 배경 이미지를 `.content-tab-active::before`
|
|
827
|
+
* (position:fixed, z-index:-1) 로 **모든 내용 뒤에** 깐다. 그런데 xterm 은
|
|
828
|
+
* 컬러스킴의 `background` 색으로 셀을 직접 칠하므로, 그 색에 알파가 없으면
|
|
829
|
+
* 이미지는 터미널 영역에서 100% 가려진다 — "배경 이미지를 넣었는데 아무 데도 안 보인다"
|
|
830
|
+
* 의 정체가 이것이다(2026-08-28 실측: colorScheme.background = '#171717' 불투명).
|
|
831
|
+
* Tabby 는 xterm 을 `allowTransparency: true` 로 만들기 때문에 알파만 넣어주면 비친다.
|
|
832
|
+
*
|
|
833
|
+
* 그래서 agentdeck 이 기동 시 `terminal.colorScheme.background` 를 8자리 hex 로 바꿔준다.
|
|
834
|
+
* 100 으로 되돌리면 알파를 떼어 원래 색으로 복구한다 (되돌리기 가능).
|
|
835
|
+
*/
|
|
836
|
+
terminalOpacity: 75,
|
|
837
|
+
/** 사이드바 배경 불투명도 (%). 터미널과 같은 이유로 배경 이미지가 비치게 한다 */
|
|
838
|
+
sidebarOpacity: 75,
|
|
839
|
+
/**
|
|
840
|
+
* 새 탭에서 첫 화면이 깨질 때, 세션이 열린 뒤 이 시간(ms)에 pty 크기를 한 번 흔든다.
|
|
841
|
+
* 0 이면 끄기. 자세한 이유는 deck.service 의 kickPtySize 주석 참고
|
|
842
|
+
*/
|
|
843
|
+
redrawKickMs: 900,
|
|
844
|
+
/**
|
|
845
|
+
* 기동 후 이 시간(ms)에 **창을 복원→최대화로 한 번 흔든다**. 0 이면 끄기.
|
|
846
|
+
*
|
|
847
|
+
* pty 크기 흔들기(redrawKickMs)가 놓치는 경우를 위한 최종 보험이다. 사용자가 손으로
|
|
848
|
+
* 하던 바로 그 동작이고, OS 가 진짜 SIGWINCH 를 두 번 내므로 TUI 가 반드시 다시 그린다.
|
|
849
|
+
* 창이 최대화 상태일 때만 동작한다 — 아니면 창 상태를 멋대로 바꾸게 되므로.
|
|
850
|
+
*/
|
|
851
|
+
startupWindowKickMs: 1500,
|
|
852
|
+
/**
|
|
853
|
+
* 작업 루트 전용 프로필을 만들고 기본 프로필로 삼는다.
|
|
854
|
+
*
|
|
855
|
+
* 에이전트 CLI 는 "어느 폴더에서 떴는가" 가 곧 작업 대상이라 새 탭마다 cd 하게 되는데,
|
|
856
|
+
* 그걸 없애려는 기능이다. 기본은 꺼져 있다 — 남의 PC 에 설치되자마자 프로필을 만들고
|
|
857
|
+
* 기본값을 가로채면 곤란하므로, 쓰려는 사람이 `rootProfileCwd` 를 채우고 켜야 한다.
|
|
858
|
+
*/
|
|
859
|
+
rootProfile: false,
|
|
860
|
+
rootProfileName: 'Agent Root',
|
|
861
|
+
// 백슬래시 이스케이프 함정을 피하려고 슬래시로 적는다 (Windows 도 그대로 받는다)
|
|
862
|
+
// 빈 값이면 프로필을 만들지 않는다
|
|
863
|
+
rootProfileCwd: '',
|
|
864
|
+
rootProfileCommand: 'powershell.exe',
|
|
865
|
+
/**
|
|
866
|
+
* -ExecutionPolicy Bypass 가 필요한 이유: npm 전역 명령(claude 등)은 .ps1 래퍼인데
|
|
867
|
+
* 기본 실행 정책(Restricted)에서는 로드가 막힌다. 이 인자는 해당 세션에만 적용되고
|
|
868
|
+
* 시스템/사용자 정책은 건드리지 않는다.
|
|
869
|
+
*/
|
|
870
|
+
rootProfileArgs: ['-NoLogo', '-ExecutionPolicy', 'Bypass'],
|
|
871
|
+
/**
|
|
872
|
+
* 탭에 주입할 환경변수. Claude Code 등 TUI 는 이 값들이 없으면 색 지원을 낮게 잡아
|
|
873
|
+
* 흑백에 가깝게 렌더된다 (Windows 는 TERM 이 기본으로 비어 있다).
|
|
874
|
+
*/
|
|
875
|
+
rootProfileEnv: {
|
|
876
|
+
TERM: 'xterm-256color',
|
|
877
|
+
COLORTERM: 'truecolor',
|
|
878
|
+
FORCE_COLOR: '3',
|
|
879
|
+
},
|
|
880
|
+
/** true 면 탭마다 UAC 승격 (Tabby 자체를 관리자로 띄웠다면 불필요) */
|
|
881
|
+
rootProfileAdmin: false,
|
|
882
|
+
/** 기본 프로필 지정을 이미 했는지 — 사용자 선택을 매번 덮어쓰지 않기 위한 플래그 */
|
|
883
|
+
rootProfileClaimed: false,
|
|
884
|
+
},
|
|
885
|
+
hotkeys: {
|
|
886
|
+
'agentdeck-toggle': [],
|
|
887
|
+
/**
|
|
888
|
+
* Claude Code 는 0x0A(Ctrl+J)를 줄바꿈으로 받는다. Tabby 로 옮겨 왔으니
|
|
889
|
+
* Ctrl+Enter / Shift+Enter 로도 같은 바이트를 보낸다
|
|
890
|
+
*/
|
|
891
|
+
'agentdeck-newline': ['Ctrl-Enter', 'Shift-Enter'],
|
|
892
|
+
/**
|
|
893
|
+
* Tabby 순정 `paste` 대신 이걸 쓴다 — 클립보드가 이미지인지 텍스트인지 보고
|
|
894
|
+
* 갈라야 하는데 순정 paste 는 텍스트만 알기 때문
|
|
895
|
+
*/
|
|
896
|
+
'agentdeck-paste': ['Ctrl-V'],
|
|
897
|
+
},
|
|
898
|
+
};
|
|
899
|
+
}
|
|
900
|
+
}
|
|
901
|
+
|
|
902
|
+
|
|
903
|
+
/***/ },
|
|
904
|
+
|
|
905
|
+
/***/ "./src/deck.service.ts"
|
|
906
|
+
/*!*****************************!*\
|
|
907
|
+
!*** ./src/deck.service.ts ***!
|
|
908
|
+
\*****************************/
|
|
909
|
+
(__unused_webpack_module, __webpack_exports__, __webpack_require__) {
|
|
910
|
+
|
|
911
|
+
__webpack_require__.r(__webpack_exports__);
|
|
912
|
+
/* harmony export */ __webpack_require__.d(__webpack_exports__, {
|
|
913
|
+
/* harmony export */ AgentDeckService: () => (/* binding */ AgentDeckService)
|
|
914
|
+
/* harmony export */ });
|
|
915
|
+
/* harmony import */ var _angular_core__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! @angular/core */ "@angular/core");
|
|
916
|
+
/* harmony import */ var _angular_core__WEBPACK_IMPORTED_MODULE_0___default = /*#__PURE__*/__webpack_require__.n(_angular_core__WEBPACK_IMPORTED_MODULE_0__);
|
|
917
|
+
/* harmony import */ var tabby_core__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(/*! tabby-core */ "tabby-core");
|
|
918
|
+
/* harmony import */ var tabby_core__WEBPACK_IMPORTED_MODULE_1___default = /*#__PURE__*/__webpack_require__.n(tabby_core__WEBPACK_IMPORTED_MODULE_1__);
|
|
919
|
+
/* harmony import */ var tabby_settings__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(/*! tabby-settings */ "tabby-settings");
|
|
920
|
+
/* harmony import */ var tabby_settings__WEBPACK_IMPORTED_MODULE_2___default = /*#__PURE__*/__webpack_require__.n(tabby_settings__WEBPACK_IMPORTED_MODULE_2__);
|
|
921
|
+
/* harmony import */ var fs__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(/*! fs */ "fs");
|
|
922
|
+
/* harmony import */ var fs__WEBPACK_IMPORTED_MODULE_3___default = /*#__PURE__*/__webpack_require__.n(fs__WEBPACK_IMPORTED_MODULE_3__);
|
|
923
|
+
/* harmony import */ var os__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(/*! os */ "os");
|
|
924
|
+
/* harmony import */ var os__WEBPACK_IMPORTED_MODULE_4___default = /*#__PURE__*/__webpack_require__.n(os__WEBPACK_IMPORTED_MODULE_4__);
|
|
925
|
+
/* harmony import */ var path__WEBPACK_IMPORTED_MODULE_5__ = __webpack_require__(/*! path */ "path");
|
|
926
|
+
/* harmony import */ var path__WEBPACK_IMPORTED_MODULE_5___default = /*#__PURE__*/__webpack_require__.n(path__WEBPACK_IMPORTED_MODULE_5__);
|
|
927
|
+
/* harmony import */ var _status_service__WEBPACK_IMPORTED_MODULE_6__ = __webpack_require__(/*! ./status.service */ "./src/status.service.ts");
|
|
928
|
+
/* harmony import */ var _detect__WEBPACK_IMPORTED_MODULE_7__ = __webpack_require__(/*! ./detect */ "./src/detect.ts");
|
|
929
|
+
/* harmony import */ var _api__WEBPACK_IMPORTED_MODULE_8__ = __webpack_require__(/*! ./api */ "./src/api.ts");
|
|
930
|
+
/* harmony import */ var _prompt__WEBPACK_IMPORTED_MODULE_9__ = __webpack_require__(/*! ./prompt */ "./src/prompt.ts");
|
|
931
|
+
var __decorate = (undefined && undefined.__decorate) || function (decorators, target, key, desc) {
|
|
932
|
+
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
933
|
+
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
|
934
|
+
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;
|
|
935
|
+
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
936
|
+
};
|
|
937
|
+
var __metadata = (undefined && undefined.__metadata) || function (k, v) {
|
|
938
|
+
if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
|
|
939
|
+
};
|
|
940
|
+
|
|
941
|
+
|
|
942
|
+
|
|
943
|
+
|
|
944
|
+
|
|
945
|
+
|
|
946
|
+
|
|
947
|
+
|
|
948
|
+
|
|
949
|
+
|
|
950
|
+
const SIDEBAR_ID = 'agentdeck-sidebar';
|
|
951
|
+
const BODY_CLASS = 'agentdeck-active';
|
|
952
|
+
/** pty 흔들기 재시도 간격/횟수 — 첫 기동 때 셸이 늦게 열리는 구간을 덮는다 (최대 약 6초) */
|
|
953
|
+
const KICK_RETRY_MS = 400;
|
|
954
|
+
const KICK_RETRY_MAX = 15;
|
|
955
|
+
/**
|
|
956
|
+
* 기동 직후 이 시간(ms) 안에 `.content.main` 폭이 실제로 바뀌면 그때 열려 있던 pane 을 다시 흔든다.
|
|
957
|
+
* 첫 흔들기가 폭 변경보다 먼저 끝나버리면 TUI 는 다시 옛 폭 화면으로 남기 때문이다.
|
|
958
|
+
*/
|
|
959
|
+
const KICK_REARM_MS = 8000;
|
|
960
|
+
/** Ctrl+V 를 눌렀을 때 xterm 의 네이티브 paste 가 오는지 기다려보는 시간 */
|
|
961
|
+
const PASTE_NATIVE_WAIT_MS = 60;
|
|
962
|
+
/**
|
|
963
|
+
* 같은 한 번의 붙여넣기가 여러 경로로 들어왔을 때 뒤엣것을 버리는 창(ms).
|
|
964
|
+
*
|
|
965
|
+
* 사람이 Ctrl+V 를 연타해도 이보다 빠를 수 없다(실측된 중복은 1ms 간격이었다).
|
|
966
|
+
* claimCtrlVKey 로 경로를 하나로 모았어도, 그게 안 먹는 상황(포커스가 xterm 밖 등)을
|
|
967
|
+
* 대비한 마지막 보험이다.
|
|
968
|
+
*/
|
|
969
|
+
const PASTE_DEDUPE_MS = 80;
|
|
970
|
+
/** 붙여넣기 계측 창 — 두 번째 write 가 늦게 와도 놓치지 않게 넉넉히 둔다 */
|
|
971
|
+
const PASTE_PROBE_MS = 5000;
|
|
972
|
+
/** 입력창을 찾으려고 화면 아래에서 훑어 올라갈 줄 수 — 여러 줄 프롬프트를 덮을 만큼만 */
|
|
973
|
+
const PROMPT_SCAN_ROWS = 14;
|
|
974
|
+
/** 진단 로그 — devtools 를 못 여는 상황에서도 밖에서 읽을 수 있게 파일로 남긴다 */
|
|
975
|
+
const DIAG_PATH = path__WEBPACK_IMPORTED_MODULE_5__.join(os__WEBPACK_IMPORTED_MODULE_4__.homedir(), '.agentdeck-diag.log');
|
|
976
|
+
let cachedClipboard;
|
|
977
|
+
/**
|
|
978
|
+
* Electron 의 clipboard 를 런타임에 가져온다.
|
|
979
|
+
*
|
|
980
|
+
* `import { clipboard } from 'electron'` 을 쓰면 타입 때문에 electron 패키지(200MB+)를
|
|
981
|
+
* devDependency 로 받아야 한다. Tabby 렌더러는 nodeIntegration 이 켜져 있어(`fs` 를 그냥 import 한다)
|
|
982
|
+
* window.require 로 바로 집을 수 있으므로 그쪽을 쓴다.
|
|
983
|
+
*/
|
|
984
|
+
function getClipboard() {
|
|
985
|
+
var _a, _b, _c, _d, _e;
|
|
986
|
+
if (cachedClipboard !== undefined) {
|
|
987
|
+
return cachedClipboard;
|
|
988
|
+
}
|
|
989
|
+
// webpack externals 에 'electron' 이 있어(webpack.config.js) 이 require 는 번들되지 않고
|
|
990
|
+
// 런타임에 Tabby 가 그대로 넘겨준다 — 위쪽 `import * as fs` 와 같은 경로다.
|
|
991
|
+
// window.require 는 contextIsolation 이 켜지면 사라지므로 폴백으로만 둔다.
|
|
992
|
+
try {
|
|
993
|
+
cachedClipboard = (_b = (_a = __webpack_require__(/*! electron */ "electron")) === null || _a === void 0 ? void 0 : _a.clipboard) !== null && _b !== void 0 ? _b : null;
|
|
994
|
+
}
|
|
995
|
+
catch {
|
|
996
|
+
cachedClipboard = null;
|
|
997
|
+
}
|
|
998
|
+
if (!cachedClipboard) {
|
|
999
|
+
try {
|
|
1000
|
+
const req = (_c = window.require) !== null && _c !== void 0 ? _c : globalThis.require;
|
|
1001
|
+
cachedClipboard = (_e = (_d = req === null || req === void 0 ? void 0 : req('electron')) === null || _d === void 0 ? void 0 : _d.clipboard) !== null && _e !== void 0 ? _e : null;
|
|
1002
|
+
}
|
|
1003
|
+
catch {
|
|
1004
|
+
cachedClipboard = null;
|
|
1005
|
+
}
|
|
1006
|
+
}
|
|
1007
|
+
return cachedClipboard;
|
|
1008
|
+
}
|
|
1009
|
+
function clampPct(value) {
|
|
1010
|
+
const n = typeof value === 'number' && isFinite(value) ? value : 100;
|
|
1011
|
+
return Math.min(100, Math.max(0, Math.round(n)));
|
|
1012
|
+
}
|
|
1013
|
+
/**
|
|
1014
|
+
* `#RRGGBB` / `#RRGGBBAA` 에 불투명도(%)를 입힌다.
|
|
1015
|
+
* 100% 면 알파를 떼어 원래 6자리로 되돌린다 — 설정을 되돌리면 원상복구되게 하려는 것.
|
|
1016
|
+
* 해석할 수 없는 형식(rgb(), 색이름 등)은 건드리지 않고 그대로 돌려준다.
|
|
1017
|
+
*/
|
|
1018
|
+
function withAlpha(color, pct) {
|
|
1019
|
+
const m = /^#([0-9a-fA-F]{6})(?:[0-9a-fA-F]{2})?$/.exec(color.trim());
|
|
1020
|
+
if (!m) {
|
|
1021
|
+
return color;
|
|
1022
|
+
}
|
|
1023
|
+
const rgb = '#' + m[1];
|
|
1024
|
+
if (pct >= 100) {
|
|
1025
|
+
return rgb;
|
|
1026
|
+
}
|
|
1027
|
+
const alpha = Math.round(pct * 255 / 100).toString(16).padStart(2, '0');
|
|
1028
|
+
return rgb + alpha;
|
|
1029
|
+
}
|
|
1030
|
+
/**
|
|
1031
|
+
* AgentDeck 본체.
|
|
1032
|
+
*
|
|
1033
|
+
* 하는 일 두 가지 —
|
|
1034
|
+
* 1) 터미널 뷰포트(.content.main)를 창 높이 기준 4:3 폭으로 고정하고
|
|
1035
|
+
* 2) 그때 남는 왼쪽 공간에 탭 목록 + 작업 상태 사이드바를 그린다.
|
|
1036
|
+
*
|
|
1037
|
+
* Tabby 순정 탭바는 CSS 로 감추고 이 사이드바가 탭 관리를 대신한다.
|
|
1038
|
+
* Angular 컴포넌트 대신 순수 DOM 을 쓰는 이유는 appRoot 템플릿에 컴포넌트를
|
|
1039
|
+
* 꽂는 공식 확장점이 없기 때문 — DOM 직접 조작이 Tabby 버전 변화에 더 견딘다.
|
|
1040
|
+
*/
|
|
1041
|
+
let AgentDeckService = class AgentDeckService {
|
|
1042
|
+
constructor(app, config, status, hotkeys, profiles, hostWindow, zone) {
|
|
1043
|
+
this.app = app;
|
|
1044
|
+
this.config = config;
|
|
1045
|
+
this.status = status;
|
|
1046
|
+
this.hotkeys = hotkeys;
|
|
1047
|
+
this.profiles = profiles;
|
|
1048
|
+
this.hostWindow = hostWindow;
|
|
1049
|
+
this.zone = zone;
|
|
1050
|
+
this.sidebar = null;
|
|
1051
|
+
this.listEl = null;
|
|
1052
|
+
this.windowEl = null;
|
|
1053
|
+
this.mainEl = null;
|
|
1054
|
+
this.observer = null;
|
|
1055
|
+
this.editing = null;
|
|
1056
|
+
this.renderQueued = false;
|
|
1057
|
+
/** rAF 가 멈춘 상황(창 가려짐)을 대비한 렌더 폴백 타이머 */
|
|
1058
|
+
this.renderTimer = null;
|
|
1059
|
+
/** 직전에 적용한 폭 — 같은 값이면 DOM 을 다시 건드리지 않는다 */
|
|
1060
|
+
this.lastTermW = -1;
|
|
1061
|
+
this.lastSidebarW = -1;
|
|
1062
|
+
this.refitQueued = false;
|
|
1063
|
+
/** 이미 감시를 건 래퍼 탭 / pane — 중복 구독 방지 */
|
|
1064
|
+
this.watched = new WeakSet();
|
|
1065
|
+
this.watchedPanes = new WeakSet();
|
|
1066
|
+
/** 첫 화면 복구용 pty 흔들기를 이미 한 pane — 탭당 한 번만 한다 */
|
|
1067
|
+
this.kickedPanes = new WeakSet();
|
|
1068
|
+
/** setup() 시각 — 기동 직후 구간인지 판단한다 (0 이면 아직 setup 전) */
|
|
1069
|
+
this.setupAt = 0;
|
|
1070
|
+
/** 기동 직후 구간에 잡힌 pane 들 — 폭이 바뀌면 이들만 다시 흔든다. 구간이 지나면 비운다 */
|
|
1071
|
+
this.startupPanes = [];
|
|
1072
|
+
/** 진단 기록 — devtools 의 `__agentdeck.kickLog` 와 DIAG_PATH 파일에 함께 남는다 */
|
|
1073
|
+
this.kickLog = [];
|
|
1074
|
+
/** diagOnce 가 이미 남긴 키 */
|
|
1075
|
+
this.diagOnceKeys = new Set();
|
|
1076
|
+
/** 우클릭 누른 시각 — 길게 누르면 컨텍스트 메뉴로 분기 */
|
|
1077
|
+
this.rightDownAt = 0;
|
|
1078
|
+
/** 직전 우클릭 처리 시각 — 같은 클릭이 두 번 들어오는 걸 막는다 */
|
|
1079
|
+
this.lastRightHandledAt = 0;
|
|
1080
|
+
/** xterm 이 네이티브 paste 로 붙여넣은 마지막 시각 — Ctrl+V 이중 입력 방지 */
|
|
1081
|
+
this.nativePasteAt = 0;
|
|
1082
|
+
/** 직전에 붙여넣기를 처리한 시각 — 같은 클릭/키가 여러 경로로 들어와도 한 번만 붙인다 */
|
|
1083
|
+
this.lastPasteAt = 0;
|
|
1084
|
+
/** 이 시각까지는 PTY 로 나가는 입력을 진단 로그에 남긴다 (붙여넣기가 두 번 들어가는지 보려고) */
|
|
1085
|
+
this.pasteProbeUntil = 0;
|
|
1086
|
+
}
|
|
1087
|
+
init() {
|
|
1088
|
+
this.app.ready$.subscribe(() => this.zone.runOutsideAngular(() => {
|
|
1089
|
+
var _a;
|
|
1090
|
+
// setup 도중 예외가 나면 그 뒤 배선(탭 감시, 흔들기 예약)이 통째로 죽는데
|
|
1091
|
+
// 콘솔을 안 보면 알 길이 없다. 진단 파일에 남겨 밖에서 읽을 수 있게 한다
|
|
1092
|
+
try {
|
|
1093
|
+
this.setup();
|
|
1094
|
+
}
|
|
1095
|
+
catch (e) {
|
|
1096
|
+
this.diag(`${new Date().toISOString()} setup ERROR ${(_a = e === null || e === void 0 ? void 0 : e.message) !== null && _a !== void 0 ? _a : e}`);
|
|
1097
|
+
throw e;
|
|
1098
|
+
}
|
|
1099
|
+
}));
|
|
1100
|
+
}
|
|
1101
|
+
/**
|
|
1102
|
+
* 같은 키로는 한 번만 남기는 진단.
|
|
1103
|
+
* relayout 처럼 초당 여러 번 불리는 자리에서 조기 return 사유를 남길 때 쓴다
|
|
1104
|
+
*/
|
|
1105
|
+
diagOnce(key, line) {
|
|
1106
|
+
if (this.diagOnceKeys.has(key)) {
|
|
1107
|
+
return;
|
|
1108
|
+
}
|
|
1109
|
+
this.diagOnceKeys.add(key);
|
|
1110
|
+
this.diag(`${new Date().toISOString()} ${line}`);
|
|
1111
|
+
}
|
|
1112
|
+
/** 진단 한 줄 — 메모리와 파일에 함께 남긴다. 파일이 안 써져도 동작에는 영향이 없다 */
|
|
1113
|
+
diag(line) {
|
|
1114
|
+
this.kickLog.push(line);
|
|
1115
|
+
try {
|
|
1116
|
+
fs__WEBPACK_IMPORTED_MODULE_3__.appendFileSync(DIAG_PATH, line + '\n');
|
|
1117
|
+
}
|
|
1118
|
+
catch {
|
|
1119
|
+
// 디스크가 막혀 있다 — 진단일 뿐이라 무시한다
|
|
1120
|
+
}
|
|
1121
|
+
}
|
|
1122
|
+
setup() {
|
|
1123
|
+
this.windowEl = document.querySelector('.window');
|
|
1124
|
+
this.mainEl = document.querySelector('.content.main');
|
|
1125
|
+
if (!this.windowEl) {
|
|
1126
|
+
console.warn('[agentdeck] .window 를 찾지 못해 레이아웃을 적용하지 않는다');
|
|
1127
|
+
return;
|
|
1128
|
+
}
|
|
1129
|
+
try {
|
|
1130
|
+
fs__WEBPACK_IMPORTED_MODULE_3__.writeFileSync(DIAG_PATH, '');
|
|
1131
|
+
}
|
|
1132
|
+
catch {
|
|
1133
|
+
// 진단 파일을 못 열어도 그냥 간다
|
|
1134
|
+
}
|
|
1135
|
+
this.setupAt = Date.now();
|
|
1136
|
+
this.diag(`${new Date().toISOString()} setup start tabs=${this.app.tabs.length}`);
|
|
1137
|
+
this.buildSidebar();
|
|
1138
|
+
this.applyEnabled();
|
|
1139
|
+
this.applyOpacity();
|
|
1140
|
+
this.disableBackgroundThrottling();
|
|
1141
|
+
this.ensurePasteHotkey();
|
|
1142
|
+
this.ensureNewlineHotkey();
|
|
1143
|
+
this.claimCtrlVKey();
|
|
1144
|
+
this.claimEnterLabel();
|
|
1145
|
+
this.claimRightClick();
|
|
1146
|
+
// xterm 의 네이티브 paste 를 관측만 한다 (doPaste 의 이중 입력 방지용).
|
|
1147
|
+
// 캡처 단계라 xterm 핸들러의 stopPropagation 보다 먼저 돈다.
|
|
1148
|
+
document.addEventListener('paste', () => {
|
|
1149
|
+
this.nativePasteAt = Date.now();
|
|
1150
|
+
this.diag(`${new Date().toISOString()} native-paste`);
|
|
1151
|
+
}, true);
|
|
1152
|
+
// 탭 변화 -> 사이드바 다시 그림
|
|
1153
|
+
// 탭 복원은 ready$ 보다 늦게 끝나므로 목록이 바뀔 때마다 감시 대상을 다시 훑는다
|
|
1154
|
+
// (watched WeakSet 이 중복 구독을 막는다)
|
|
1155
|
+
this.app.tabsChanged$.subscribe(() => {
|
|
1156
|
+
for (const tab of this.app.tabs) {
|
|
1157
|
+
this.watchTab(tab);
|
|
1158
|
+
}
|
|
1159
|
+
this.scheduleRender();
|
|
1160
|
+
});
|
|
1161
|
+
// 탭을 바꾸면 그 탭은 숨어 있던 동안 폭 0 이라 refit 을 건너뛰었다 — 지금 맞춰준다
|
|
1162
|
+
this.app.activeTabChange$.subscribe(() => {
|
|
1163
|
+
this.scheduleRender();
|
|
1164
|
+
this.scheduleRefit();
|
|
1165
|
+
});
|
|
1166
|
+
this.app.tabOpened$.subscribe(tab => {
|
|
1167
|
+
this.watchTab(tab);
|
|
1168
|
+
// 새 탭은 xterm 은 맞는 폭으로 열리는데 pty 만 옛 폭으로 남는 일이 있다 (syncPtySize 주석)
|
|
1169
|
+
this.scheduleRefit();
|
|
1170
|
+
this.scheduleRender();
|
|
1171
|
+
});
|
|
1172
|
+
this.app.tabClosed$.subscribe(() => this.scheduleRender());
|
|
1173
|
+
// 세션이 이미 열린 채 복원된 탭들도 감시 대상에 넣는다
|
|
1174
|
+
for (const tab of this.app.tabs) {
|
|
1175
|
+
this.watchTab(tab);
|
|
1176
|
+
}
|
|
1177
|
+
this.status.changed$.subscribe(() => this.scheduleRender());
|
|
1178
|
+
this.config.changed$.subscribe(() => {
|
|
1179
|
+
this.applyEnabled();
|
|
1180
|
+
this.applyOpacity();
|
|
1181
|
+
this.relayout();
|
|
1182
|
+
this.scheduleRender();
|
|
1183
|
+
});
|
|
1184
|
+
// 창 크기 변화 -> 4:3 폭 재계산
|
|
1185
|
+
this.observer = new ResizeObserver(() => this.relayout());
|
|
1186
|
+
this.observer.observe(this.windowEl);
|
|
1187
|
+
window.addEventListener('resize', () => this.relayout());
|
|
1188
|
+
// 핫키 처리는 여기서 — provider 쪽에서 하면 HotkeysService 와 순환 의존이 된다
|
|
1189
|
+
this.hotkeys.hotkey$.subscribe(hotkey => {
|
|
1190
|
+
if (hotkey === 'agentdeck-toggle') {
|
|
1191
|
+
this.config.store.agentDeck.enabled = !this.config.store.agentDeck.enabled;
|
|
1192
|
+
this.config.save();
|
|
1193
|
+
}
|
|
1194
|
+
if (hotkey === 'agentdeck-newline') {
|
|
1195
|
+
this.sendNewline();
|
|
1196
|
+
}
|
|
1197
|
+
if (hotkey === 'agentdeck-paste') {
|
|
1198
|
+
this.doPaste('hotkey');
|
|
1199
|
+
}
|
|
1200
|
+
});
|
|
1201
|
+
// 경과 시간 갱신 + running -> idle 자동 복귀
|
|
1202
|
+
setInterval(() => this.tick(), 1000);
|
|
1203
|
+
this.scheduleWindowKick();
|
|
1204
|
+
this.relayout();
|
|
1205
|
+
this.render();
|
|
1206
|
+
this.diag(`${new Date().toISOString()} setup done tabs=${this.app.tabs.length} enabled=${this.enabled} mainEl=${!!this.mainEl}`);
|
|
1207
|
+
window.__agentdeck = {
|
|
1208
|
+
app: this.app,
|
|
1209
|
+
config: this.config,
|
|
1210
|
+
status: this.status,
|
|
1211
|
+
relayout: () => this.relayout(),
|
|
1212
|
+
render: () => this.render(),
|
|
1213
|
+
kickLog: this.kickLog,
|
|
1214
|
+
};
|
|
1215
|
+
}
|
|
1216
|
+
// ---------- 세션 감시 ----------
|
|
1217
|
+
/**
|
|
1218
|
+
* 탭(그리고 그 안의 모든 pane)의 PTY 출력을 구독한다.
|
|
1219
|
+
*
|
|
1220
|
+
* TerminalDecorator 는 프론트엔드 첫 리사이즈 시점에만 붙어서 복원된 탭 등을
|
|
1221
|
+
* 놓치는 경우가 있다. 이쪽은 탭이 목록에 들어오는 즉시 붙으므로 그 빈틈을 메운다.
|
|
1222
|
+
* 같은 출력을 decorator 와 둘 다 먹어도 상태 설정이 멱등이라 결과는 같다.
|
|
1223
|
+
*/
|
|
1224
|
+
watchTab(root) {
|
|
1225
|
+
const anyRoot = root;
|
|
1226
|
+
if (!this.watched.has(root)) {
|
|
1227
|
+
this.watched.add(root);
|
|
1228
|
+
// 제목은 래퍼 탭이 자식 것을 위임받으므로 여기서 받는 게 가장 확실하다
|
|
1229
|
+
if (anyRoot.titleChange$) {
|
|
1230
|
+
anyRoot.titleChange$.subscribe((title) => {
|
|
1231
|
+
(0,_detect__WEBPACK_IMPORTED_MODULE_7__.applyTitle)(this.status, root, title, this.config.store.agentDeck.autoDetect);
|
|
1232
|
+
});
|
|
1233
|
+
}
|
|
1234
|
+
if (anyRoot.title) {
|
|
1235
|
+
(0,_detect__WEBPACK_IMPORTED_MODULE_7__.applyTitle)(this.status, root, anyRoot.title, this.config.store.agentDeck.autoDetect);
|
|
1236
|
+
}
|
|
1237
|
+
// 나중에 분할로 추가되는 pane 도 따라간다
|
|
1238
|
+
if (anyRoot.tabAdded$) {
|
|
1239
|
+
anyRoot.tabAdded$.subscribe((pane) => this.watchPane(root, pane));
|
|
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;
|
|
1266
|
+
}
|
|
1267
|
+
session.output$.subscribe((data) => {
|
|
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);
|
|
1290
|
+
}
|
|
1291
|
+
/**
|
|
1292
|
+
* 기동 직후 창을 **복원 -> 최대화**로 한 번 흔든다. 사용자가 손으로 하던 그 동작이다.
|
|
1293
|
+
*
|
|
1294
|
+
* pty 크기 흔들기(kickPtySize)는 우리가 만든 가짜 SIGWINCH 라, 세션이 늦게 열리거나
|
|
1295
|
+
* 폭 변경 타이밍이 어긋나면 놓칠 수 있다. 이쪽은 OS 가 진짜로 창 크기를 두 번 바꾸므로
|
|
1296
|
+
* 그 아래 있는 xterm, pty, TUI 가 예외 없이 다시 그린다. 최종 보험이다.
|
|
1297
|
+
*
|
|
1298
|
+
* 최대화 상태일 때만 한다. 아니면 사용자의 창 상태를 멋대로 최대화해버린다.
|
|
1299
|
+
*/
|
|
1300
|
+
scheduleWindowKick() {
|
|
1301
|
+
const delay = this.config.store.agentDeck.startupWindowKickMs;
|
|
1302
|
+
if (!delay) {
|
|
1303
|
+
this.diag(`${new Date().toISOString()} window-kick disabled startupWindowKickMs=${JSON.stringify(delay)}`);
|
|
1304
|
+
return;
|
|
1305
|
+
}
|
|
1306
|
+
this.diag(`${new Date().toISOString()} window-kick scheduled +${delay}ms`);
|
|
1307
|
+
setTimeout(() => {
|
|
1308
|
+
let maximized = false;
|
|
1309
|
+
try {
|
|
1310
|
+
maximized = this.hostWindow.isMaximized();
|
|
1311
|
+
}
|
|
1312
|
+
catch {
|
|
1313
|
+
// 창 정보를 못 읽는 호스트(웹 빌드 등)라면 그냥 넘어간다
|
|
1314
|
+
}
|
|
1315
|
+
if (!maximized) {
|
|
1316
|
+
this.diag(`${new Date().toISOString()} window-kick skipped (not maximized)`);
|
|
1317
|
+
return;
|
|
1318
|
+
}
|
|
1319
|
+
this.diag(`${new Date().toISOString()} window-kick restore`);
|
|
1320
|
+
this.hostWindow.toggleMaximize();
|
|
1321
|
+
setTimeout(() => {
|
|
1322
|
+
this.diag(`${new Date().toISOString()} window-kick maximize`);
|
|
1323
|
+
this.hostWindow.toggleMaximize();
|
|
1324
|
+
}, 220);
|
|
1325
|
+
}, delay);
|
|
1326
|
+
}
|
|
1327
|
+
/**
|
|
1328
|
+
* 탭이 열린 직후 한 번만 pty 크기를 흔들어 TUI 가 화면 전체를 다시 그리게 한다.
|
|
1329
|
+
*
|
|
1330
|
+
* 왜 필요한가 — 탭이 만들어지는 동안 우리가 `.content.main` 폭을 바꾸면 그 사이에
|
|
1331
|
+
* 이미 뜬 TUI(Claude Code 등)는 **옛 폭 기준으로 첫 화면을 그려 놓은 상태**가 된다.
|
|
1332
|
+
* 그 뒤 refit/syncPtySize 가 크기를 맞춰도, 맞춘 값이 pty 가 이미 들고 있던 값과 같으면
|
|
1333
|
+
* 커널은 SIGWINCH 를 내지 않는다 — TUI 는 "크기 안 바뀌었네" 하고 그대로 있고,
|
|
1334
|
+
* 옛 폭으로 그린 입력창 프레임 잔상이 화면에 남는다 (2026-08-28 실측: 첫 한 번만 재현,
|
|
1335
|
+
* 창 복원→최대화로 흔들면 즉시 정상 — 그게 SIGWINCH 를 두 번 내기 때문이다).
|
|
1336
|
+
*
|
|
1337
|
+
* 그래서 같은 일을 코드로 한다: 한 열 좁혔다가 곧바로 되돌린다. 두 번의 SIGWINCH 가
|
|
1338
|
+
* 확실히 전달되어 TUI 가 전체 재렌더를 하고 잔상이 지워진다. 탭당 한 번뿐이라
|
|
1339
|
+
* 정상 동작하던 탭에는 재렌더 1회 외에 영향이 없다.
|
|
1340
|
+
*/
|
|
1341
|
+
scheduleRedrawKick(pane, force = false) {
|
|
1342
|
+
if (!force && this.kickedPanes.has(pane)) {
|
|
1343
|
+
return;
|
|
1344
|
+
}
|
|
1345
|
+
const delay = this.config.store.agentDeck.redrawKickMs;
|
|
1346
|
+
if (!delay) {
|
|
1347
|
+
this.diag(`${new Date().toISOString()} kick disabled redrawKickMs=${JSON.stringify(delay)}`);
|
|
1348
|
+
return;
|
|
1349
|
+
}
|
|
1350
|
+
this.kickedPanes.add(pane);
|
|
1351
|
+
this.diag(`${new Date().toISOString()} scheduled +${delay}ms`);
|
|
1352
|
+
setTimeout(() => this.kickPtySize(pane), delay);
|
|
1353
|
+
}
|
|
1354
|
+
kickPtySize(pane, attempt = 0) {
|
|
1355
|
+
var _a, _b, _c, _d, _e;
|
|
1356
|
+
const anyPane = pane;
|
|
1357
|
+
const frontend = anyPane.frontend;
|
|
1358
|
+
const cols = (_a = frontend === null || frontend === void 0 ? void 0 : frontend.xterm) === null || _a === void 0 ? void 0 : _a.cols;
|
|
1359
|
+
const rows = (_b = frontend === null || frontend === void 0 ? void 0 : frontend.xterm) === null || _b === void 0 ? void 0 : _b.rows;
|
|
1360
|
+
const session = anyPane.session;
|
|
1361
|
+
if (!cols || !rows || cols < 2 || !(session === null || session === void 0 ? void 0 : session.open) || typeof session.resize !== 'function') {
|
|
1362
|
+
// Tabby 첫 기동은 셸이 뜨는 데 시간이 걸려 예약 시점엔 세션이 아직 안 열려 있다.
|
|
1363
|
+
// 여기서 그냥 포기하면 kickedPanes 에 이미 올라가 있어 두 번 다시 시도하지 않는다.
|
|
1364
|
+
this.diag(`${new Date().toISOString()} not-ready attempt=${attempt} cols=${cols} open=${session === null || session === void 0 ? void 0 : session.open}`);
|
|
1365
|
+
if (attempt < KICK_RETRY_MAX) {
|
|
1366
|
+
setTimeout(() => this.kickPtySize(pane, attempt + 1), KICK_RETRY_MS);
|
|
1367
|
+
}
|
|
1368
|
+
return;
|
|
1369
|
+
}
|
|
1370
|
+
this.diag(`${new Date().toISOString()} kick cols=${cols} rows=${rows} attempt=${attempt} `
|
|
1371
|
+
+ `fit=${typeof ((_c = frontend === null || frontend === void 0 ? void 0 : frontend.fitAddon) === null || _c === void 0 ? void 0 : _c.fit)}`);
|
|
1372
|
+
try {
|
|
1373
|
+
// PTY 만 흔들면 첫 기동은 고쳐지는데 새 탭은 안 고쳐진다.
|
|
1374
|
+
// 첫 기동은 창 복원→최대화(scheduleWindowKick)가 창 자체를 리사이즈해서
|
|
1375
|
+
// xterm 이 스스로 다시 계산하지만, 새 탭은 창 크기가 그대로라 그 계산이 일어나지 않는다.
|
|
1376
|
+
// TUI 가 옛 폭 화면으로 남는 건 xterm 쪽 상태이므로 xterm 도 같이 흔든다.
|
|
1377
|
+
(_e = (_d = frontend === null || frontend === void 0 ? void 0 : frontend.xterm) === null || _d === void 0 ? void 0 : _d.resize) === null || _e === void 0 ? void 0 : _e.call(_d, cols - 1, rows);
|
|
1378
|
+
session.resize(cols - 1, rows);
|
|
1379
|
+
setTimeout(() => {
|
|
1380
|
+
var _a, _b, _c, _d, _e, _f, _g;
|
|
1381
|
+
try {
|
|
1382
|
+
// fit() 이 뷰포트 실제 폭으로 되돌리면서 resize$ 를 흘려 session 까지 맞춘다.
|
|
1383
|
+
// fitAddon 이 없는 프론트엔드(웹 빌드 등)면 xterm 을 직접 되돌린다.
|
|
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 };
|
|
1402
|
+
}
|
|
1403
|
+
catch {
|
|
1404
|
+
// 세션이 닫혔다 — 되돌릴 대상이 없다
|
|
1405
|
+
}
|
|
1406
|
+
}, 80);
|
|
1407
|
+
}
|
|
1408
|
+
catch {
|
|
1409
|
+
// 세션이 막 닫혔다
|
|
1410
|
+
}
|
|
1411
|
+
}
|
|
1412
|
+
/**
|
|
1413
|
+
* Ctrl+V 를 **핫키 테이블에서 걷어낸다** (실제 처리는 claimCtrlVKey 가 한다).
|
|
1414
|
+
*
|
|
1415
|
+
* 예전에는 여기서 `agentdeck-paste` 에 Ctrl-V 를 등록했는데, 그러면 한 번 눌러 두 번 붙는
|
|
1416
|
+
* 경우가 남았다. Tabby 는 같은 keydown 을 **두 경로**로 HotkeysService 에 밀어 넣기 때문이다 —
|
|
1417
|
+
* xterm 의 커스텀 키 핸들러(`tabby-terminal/dist/index.js:40789` keyboardEventHandler →
|
|
1418
|
+
* `hotkeysService.pushKeyEvent`)와, document 에 걸린 core 리스너(`tabby-core:21425`).
|
|
1419
|
+
* `pushKeyEvent` 앞에 `timeStamp` 중복 가드가 있지만(`tabby-core:21451`) 그건 **같은**
|
|
1420
|
+
* 이벤트 객체일 때만 먹고, 두 경로가 서로 다른 이벤트(keydown/keyup 등)로 들어오면
|
|
1421
|
+
* `emitHotkeyOn` 이 두 번 나간다(실측: hotkey-paste 가 1ms 간격 2회, `.agentdeck-diag.log`).
|
|
1422
|
+
*
|
|
1423
|
+
* 그래서 핫키에 기대지 않고 캡처 단계에서 직접 한 번만 처리한다 — 우클릭을 claimRightClick
|
|
1424
|
+
* 으로 가져온 것과 같은 이유·같은 방식이다. 순정 `paste` 쪽에 Ctrl-V 가 남아 있어도 캡처가
|
|
1425
|
+
* 먼저 먹고 전파를 끊으므로 발화하지 않지만, 설정 화면이 헷갈리지 않게 양쪽 모두에서 뺀다.
|
|
1426
|
+
*
|
|
1427
|
+
* 대가: 터미널 앱에 리터럴 Ctrl+V(0x16, quoted-insert)를 보낼 수 없다.
|
|
1428
|
+
* 그게 필요하면 `agentDeck.claimCtrlV: false`.
|
|
1429
|
+
*/
|
|
1430
|
+
ensurePasteHotkey() {
|
|
1431
|
+
const hotkeys = this.config.store.hotkeys;
|
|
1432
|
+
if (!hotkeys) {
|
|
1433
|
+
return;
|
|
1434
|
+
}
|
|
1435
|
+
let changed = false;
|
|
1436
|
+
// 옛 이름(workdeck-*)으로 저장된 핫키가 config.yaml 에 남아 있으면 Ctrl-V 가 그쪽에
|
|
1437
|
+
// 먼저 매칭돼 우리 핸들러가 통째로 죽는다 — HotkeysService.matchActiveHotkey 는
|
|
1438
|
+
// 길이가 같은 후보 중 config 삽입 순서로 앞선 것 하나만 고르고, 옛 항목이 먼저 저장돼 있다
|
|
1439
|
+
// (tabby-core/dist/index.js:21558 matches.sort → matches[0].id).
|
|
1440
|
+
for (const legacy of ['workdeck-paste', 'workdeck-newline', 'workdeck-toggle']) {
|
|
1441
|
+
if (legacy in hotkeys) {
|
|
1442
|
+
delete hotkeys[legacy];
|
|
1443
|
+
changed = true;
|
|
1444
|
+
}
|
|
1445
|
+
}
|
|
1446
|
+
// 지난 버전이 넣어둔 Ctrl-V 를 회수한다. 남겨두면 캡처 경로와 겹쳐 다시 2중이 될 수 있다
|
|
1447
|
+
for (const id of ['agentdeck-paste', 'paste']) {
|
|
1448
|
+
const keys = Array.isArray(hotkeys[id]) ? hotkeys[id] : [];
|
|
1449
|
+
if (keys.includes('Ctrl-V')) {
|
|
1450
|
+
hotkeys[id] = keys.filter(k => k !== 'Ctrl-V');
|
|
1451
|
+
changed = true;
|
|
1452
|
+
}
|
|
1453
|
+
}
|
|
1454
|
+
if (changed) {
|
|
1455
|
+
this.config.save();
|
|
1456
|
+
}
|
|
1457
|
+
this.diag(`${new Date().toISOString()} hotkeys paste=${JSON.stringify(hotkeys.paste)} `
|
|
1458
|
+
+ `agentdeck-paste=${JSON.stringify(hotkeys['agentdeck-paste'])} `
|
|
1459
|
+
+ `legacy=${Object.keys(hotkeys).filter(k => k.startsWith('workdeck')).join(',') || 'none'}`);
|
|
1460
|
+
}
|
|
1461
|
+
/**
|
|
1462
|
+
* Ctrl+V 를 document **캡처 단계**에서 한 번만 처리한다.
|
|
1463
|
+
*
|
|
1464
|
+
* 캡처는 xterm 의 textarea 리스너보다도 먼저 돌기 때문에, 여기서 전파를 끊으면
|
|
1465
|
+
* 그 뒤에 있는 경로(xterm 커스텀 키 핸들러 → pushKeyEvent, core 의 document 리스너,
|
|
1466
|
+
* xterm 이 Ctrl+V 를 0x16 으로 바꿔 PTY 에 흘리는 기본 동작, 브라우저 네이티브 paste)가
|
|
1467
|
+
* 통째로 죽는다. 몇 개가 겹쳐 걸려 있든 붙여넣기는 정확히 한 번이 된다 —
|
|
1468
|
+
* 핫키로 받던 시절 남아 있던 2중 입력의 해법이다(ensurePasteHotkey 주석에 원인).
|
|
1469
|
+
*
|
|
1470
|
+
* 단, 설정 화면 같은 **평범한 입력 필드**에서는 손대지 않는다. 거기서는 Ctrl+V 가
|
|
1471
|
+
* 브라우저 기본 붙여넣기여야 하고, 우리가 가로채면 아예 못 붙여넣게 된다.
|
|
1472
|
+
*/
|
|
1473
|
+
claimCtrlVKey() {
|
|
1474
|
+
document.addEventListener('keydown', (event) => {
|
|
1475
|
+
if (!this.config.store.agentDeck.claimCtrlV) {
|
|
1476
|
+
return;
|
|
1477
|
+
}
|
|
1478
|
+
if (!event.ctrlKey || event.shiftKey || event.altKey || event.metaKey) {
|
|
1479
|
+
return;
|
|
1480
|
+
}
|
|
1481
|
+
// 한글 입력 상태에서는 key 가 'ㅍ' 로 오기도 한다 — 물리 키(code)를 같이 본다
|
|
1482
|
+
if ((event.key || '').toLowerCase() !== 'v' && event.code !== 'KeyV') {
|
|
1483
|
+
return;
|
|
1484
|
+
}
|
|
1485
|
+
if (this.isPlainInput(event.target)) {
|
|
1486
|
+
return;
|
|
1487
|
+
}
|
|
1488
|
+
event.preventDefault();
|
|
1489
|
+
event.stopPropagation();
|
|
1490
|
+
this.zone.runOutsideAngular(() => this.doPaste('ctrl-v'));
|
|
1491
|
+
}, true);
|
|
1492
|
+
}
|
|
1493
|
+
/**
|
|
1494
|
+
* Enter 를 눌러 프롬프트를 보낼 때, 그 프롬프트 원문을 작업 라벨로 삼는다.
|
|
1495
|
+
*
|
|
1496
|
+
* 왜 이게 필요한가 — 라벨을 채우는 다른 경로(훅·콘솔 제목)는 둘 다 배포판에서 못 믿는다.
|
|
1497
|
+
* 훅은 사용자가 `~/.claude/settings.json` 을 손봐야 돌고, 플러그인이 그걸 대신 깔아줄 수도 없다.
|
|
1498
|
+
* 콘솔 제목은 Claude Code 가 세션 첫 작업을 요약해 한 번 박고 그 뒤로 갱신하지 않는다
|
|
1499
|
+
* (2026-08-28 실측: 작업이 네 번 바뀌는 동안 `◑ sendInput 한 줄 vs 두 줄 원인 파악` 그대로).
|
|
1500
|
+
* 그래서 설정이 하나도 없는 상태에서 라벨을 최신으로 유지할 수 있는 건 이 경로뿐이다.
|
|
1501
|
+
*
|
|
1502
|
+
* **Enter 만 잡는다.** Shift+Enter / Ctrl+Enter 는 줄바꿈이라(agentdeck-newline) 아직 전송이
|
|
1503
|
+
* 아니고, 그때 라벨을 바꾸면 쓰다 만 문장이 박힌다. 수식키가 하나라도 눌려 있으면 그냥 흘린다.
|
|
1504
|
+
* IME 조합 중의 Enter(한글 확정)도 전송이 아니므로 제외한다.
|
|
1505
|
+
*
|
|
1506
|
+
* 관측만 하고 이벤트는 절대 막지 않는다 — 여기서 preventDefault 하면 프롬프트가 안 보내진다.
|
|
1507
|
+
*/
|
|
1508
|
+
claimEnterLabel() {
|
|
1509
|
+
document.addEventListener('keydown', (event) => {
|
|
1510
|
+
if (!this.config.store.agentDeck.enterAsLabel) {
|
|
1511
|
+
return;
|
|
1512
|
+
}
|
|
1513
|
+
if (event.shiftKey || event.ctrlKey || event.altKey || event.metaKey) {
|
|
1514
|
+
return; // 줄바꿈이지 전송이 아니다
|
|
1515
|
+
}
|
|
1516
|
+
if (event.key !== 'Enter' && event.code !== 'Enter' && event.code !== 'NumpadEnter') {
|
|
1517
|
+
return;
|
|
1518
|
+
}
|
|
1519
|
+
if (event.isComposing || event.keyCode === 229) {
|
|
1520
|
+
return; // 한글 조합 확정용 Enter
|
|
1521
|
+
}
|
|
1522
|
+
if (this.isPlainInput(event.target)) {
|
|
1523
|
+
return;
|
|
1524
|
+
}
|
|
1525
|
+
const pane = this.focusedPane();
|
|
1526
|
+
if (!pane) {
|
|
1527
|
+
this.diag(`${new Date().toISOString()} enter-label skipped (no pane)`);
|
|
1528
|
+
return;
|
|
1529
|
+
}
|
|
1530
|
+
// 화면은 아직 프롬프트가 지워지기 전이다 — 캡처 단계라 앱이 Enter 를 보기 전에 읽는다
|
|
1531
|
+
const text = this.readPromptLine(pane);
|
|
1532
|
+
// 상태는 루트 탭 단위로 관리한다 (watchTab 이 app.tabs 로 감시하는 것과 같은 단위)
|
|
1533
|
+
const tab = this.app.activeTab;
|
|
1534
|
+
this.diag(`${new Date().toISOString()} enter-label text="${text}" tab=${tab ? 'yes' : 'no'}`);
|
|
1535
|
+
if (!text || !tab) {
|
|
1536
|
+
return;
|
|
1537
|
+
}
|
|
1538
|
+
this.status.setLabel(tab, text);
|
|
1539
|
+
}, true);
|
|
1540
|
+
}
|
|
1541
|
+
/**
|
|
1542
|
+
* xterm 화면 버퍼에서 지금 입력창에 쓰여 있는 프롬프트를 읽는다 (판정은 extractPrompt).
|
|
1543
|
+
*
|
|
1544
|
+
* 화면 맨 아래가 아니라 **커서 줄**을 기준점으로 잡는다. Claude Code 는 대화가 짧으면
|
|
1545
|
+
* 입력창을 화면 위쪽에 두고 그 아래를 전부 빈 줄로 남기는데(2026-08-28 실측), 아래 14줄만
|
|
1546
|
+
* 읽으면 빈 줄만 걸려 프롬프트를 영영 못 찾는다 — 라벨이 안 바뀌던 원인이 이것이다.
|
|
1547
|
+
* 커서는 항상 입력 중인 줄에 있고, 여러 줄 프롬프트면 마지막 줄에 있으니 거기서 위로 훑으면
|
|
1548
|
+
* `> ` 머리를 만난다. 커서 아래는 힌트 줄이라 애초에 읽지 않는 편이 정확하다.
|
|
1549
|
+
*/
|
|
1550
|
+
readPromptLine(pane) {
|
|
1551
|
+
var _a, _b, _c;
|
|
1552
|
+
const xterm = (_a = pane.frontend) === null || _a === void 0 ? void 0 : _a.xterm;
|
|
1553
|
+
const buf = (_b = xterm === null || xterm === void 0 ? void 0 : xterm.buffer) === null || _b === void 0 ? void 0 : _b.active;
|
|
1554
|
+
if (!buf || typeof buf.getLine !== 'function') {
|
|
1555
|
+
this.diag(`${new Date().toISOString()} enter-label no-buffer frontend=${pane.frontend ? 'yes' : 'no'} xterm=${xterm ? 'yes' : 'no'}`);
|
|
1556
|
+
return '';
|
|
1557
|
+
}
|
|
1558
|
+
const read = (from, to) => {
|
|
1559
|
+
var _a, _b;
|
|
1560
|
+
const lines = [];
|
|
1561
|
+
for (let y = Math.max(0, from); y <= to; y++) {
|
|
1562
|
+
lines.push((_b = (_a = buf.getLine(y)) === null || _a === void 0 ? void 0 : _a.translateToString(true)) !== null && _b !== void 0 ? _b : '');
|
|
1563
|
+
}
|
|
1564
|
+
return lines;
|
|
1565
|
+
};
|
|
1566
|
+
const cursor = buf.baseY + ((_c = buf.cursorY) !== null && _c !== void 0 ? _c : 0);
|
|
1567
|
+
const nearCursor = read(cursor - PROMPT_SCAN_ROWS, cursor);
|
|
1568
|
+
const byCursor = (0,_prompt__WEBPACK_IMPORTED_MODULE_9__.extractPrompt)(nearCursor);
|
|
1569
|
+
if (byCursor) {
|
|
1570
|
+
return byCursor;
|
|
1571
|
+
}
|
|
1572
|
+
// 커서가 입력창 밖에 있을 때의 폴백 — 화면 전체를 아래에서 위로 훑는다
|
|
1573
|
+
const byScreen = (0,_prompt__WEBPACK_IMPORTED_MODULE_9__.extractPrompt)(read(buf.baseY, buf.baseY + xterm.rows - 1));
|
|
1574
|
+
if (!byScreen) {
|
|
1575
|
+
// 두 경로가 다 빈손이면 화면이 어떻게 생겼는지 남긴다.
|
|
1576
|
+
// 이게 없어서 "입력창 못 찾음" 과 "버퍼를 못 읽음" 을 못 갈랐다 (2026-09-01).
|
|
1577
|
+
const dump = nearCursor.slice(-4)
|
|
1578
|
+
.map(l => JSON.stringify(l.slice(0, 60)))
|
|
1579
|
+
.join(' | ');
|
|
1580
|
+
this.diag(`${new Date().toISOString()} enter-label miss baseY=${buf.baseY} cursorY=${buf.cursorY} rows=${xterm.rows} tail=${dump}`);
|
|
1581
|
+
}
|
|
1582
|
+
return byScreen;
|
|
1583
|
+
}
|
|
1584
|
+
/** 터미널이 아닌 보통 입력 필드(설정 화면 등)인지 — 거기서는 Ctrl+V 를 건드리지 않는다 */
|
|
1585
|
+
isPlainInput(target) {
|
|
1586
|
+
var _a;
|
|
1587
|
+
const el = target;
|
|
1588
|
+
if (!el || typeof el.closest !== 'function') {
|
|
1589
|
+
return false;
|
|
1590
|
+
}
|
|
1591
|
+
// xterm 이 키 입력을 받는 숨은 textarea 는 터미널이지 입력 필드가 아니다
|
|
1592
|
+
if (((_a = el.classList) === null || _a === void 0 ? void 0 : _a.contains('xterm-helper-textarea')) || el.closest('.xterm')) {
|
|
1593
|
+
return false;
|
|
1594
|
+
}
|
|
1595
|
+
return el.isContentEditable || ['INPUT', 'TEXTAREA'].includes(el.tagName);
|
|
1596
|
+
}
|
|
1597
|
+
/**
|
|
1598
|
+
* Ctrl+V 붙여넣기 — 클립보드가 이미지면 Alt+V 로 넘긴다.
|
|
1599
|
+
*
|
|
1600
|
+
* 터미널 붙여넣기는 "클립보드 텍스트를 키 입력처럼 PTY 에 써넣는" 동작이다
|
|
1601
|
+
* (`tabby-terminal/dist/index.js:41842` paste() → readClipboard() → sendInput).
|
|
1602
|
+
* PTY 는 바이트 스트림이라 이미지가 지나갈 방법 자체가 없고, 클립보드는 OS 자원이라
|
|
1603
|
+
* 터미널 안에서 도는 앱은 손댈 수 없다 — 그래서 이미지는 조용히 사라진다.
|
|
1604
|
+
*
|
|
1605
|
+
* Claude Code 는 이걸 자기가 OS 클립보드를 직접 읽어서 푼다. Alt+V(= ESC v)가 그
|
|
1606
|
+
* "지금 클립보드를 읽어라" 신호다. 즉 Ctrl+V 와 Alt+V 는 붙여넣는 주체가 다르다
|
|
1607
|
+
* (Ctrl+V = 터미널이 넣어줌 / Alt+V = 앱이 직접 읽음). 사용자가 그걸 구분할 이유는 없으니
|
|
1608
|
+
* 클립보드에 이미지만 있을 때 Ctrl+V 가 알아서 ESC v 를 대신 보낸다.
|
|
1609
|
+
*
|
|
1610
|
+
* 텍스트가 같이 들어 있으면(엑셀·워드 복사 등) 텍스트 붙여넣기를 유지한다 — 그쪽이 의도이므로.
|
|
1611
|
+
*/
|
|
1612
|
+
doPaste(source) {
|
|
1613
|
+
// 한 번의 Ctrl+V / 우클릭이 여러 경로로 들어오는 일이 있다 (ensurePasteHotkey 주석 참고).
|
|
1614
|
+
// 어느 경로가 먼저 오든 처음 것만 살린다 — 사람 연타는 이 창보다 느리다.
|
|
1615
|
+
const now = Date.now();
|
|
1616
|
+
if (now - this.lastPasteAt < PASTE_DEDUPE_MS) {
|
|
1617
|
+
this.diag(`${new Date().toISOString()} paste ignored (dup ${now - this.lastPasteAt}ms, ${source})`);
|
|
1618
|
+
return;
|
|
1619
|
+
}
|
|
1620
|
+
this.lastPasteAt = now;
|
|
1621
|
+
const pane = this.focusedPane();
|
|
1622
|
+
if (!pane) {
|
|
1623
|
+
return;
|
|
1624
|
+
}
|
|
1625
|
+
const imageOnly = this.clipboardHasImageOnly();
|
|
1626
|
+
this.diag(`${new Date().toISOString()} paste probe enabled=${this.config.store.agentDeck.pasteImageWithCtrlV} imageOnly=${imageOnly} sendInput=${typeof pane.sendInput}`);
|
|
1627
|
+
if (this.config.store.agentDeck.pasteImageWithCtrlV
|
|
1628
|
+
&& imageOnly
|
|
1629
|
+
&& typeof pane.sendInput === 'function') {
|
|
1630
|
+
// 이미지 전용 클립보드 — 텍스트 붙여넣기는 어차피 아무것도 못 넣으므로
|
|
1631
|
+
// "네가 직접 클립보드를 읽어라" 키를 흘려보낸다 (sendImagePasteKey 주석 참고)
|
|
1632
|
+
void this.sendImagePasteKey(pane);
|
|
1633
|
+
return;
|
|
1634
|
+
}
|
|
1635
|
+
if (typeof pane.paste !== 'function') {
|
|
1636
|
+
return;
|
|
1637
|
+
}
|
|
1638
|
+
// 붙여넣기 한 번이 PTY 에 몇 번 써지는지 본다 (우클릭 2중 입력 추적용).
|
|
1639
|
+
// 창을 열어둔 동안의 입력만 남긴다 — 상시 켜두면 사람이 타이핑한 것까지 다 찍힌다.
|
|
1640
|
+
this.pasteProbeUntil = Date.now() + PASTE_PROBE_MS;
|
|
1641
|
+
this.probeSendInput(pane);
|
|
1642
|
+
this.probeWrite(pane);
|
|
1643
|
+
// 텍스트 붙여넣기는 xterm 이 네이티브 paste 이벤트로 이미 처리한다
|
|
1644
|
+
// (tabby-terminal/dist/index.js:5383 에서 textarea/element 에 handlePasteEvent 등록).
|
|
1645
|
+
// 그런데도 우리가 무조건 paste() 를 부르면 한 번 눌러 두 번 붙는다.
|
|
1646
|
+
// 그래서 네이티브가 오는지 잠깐 보고, 안 오면(터미널 textarea 가 포커스를 못 잡은 경우 등)
|
|
1647
|
+
// 그제야 우리가 붙인다 — 어느 쪽이든 정확히 한 번이 된다.
|
|
1648
|
+
this.diag(`${new Date().toISOString()} paste (${source})`);
|
|
1649
|
+
if (source === 'ctrl-v') {
|
|
1650
|
+
// 캡처 단계에서 이미 preventDefault 했으므로 네이티브 paste 는 오지 않는다 — 기다릴 것 없이 바로 붙인다
|
|
1651
|
+
void pane.paste();
|
|
1652
|
+
return;
|
|
1653
|
+
}
|
|
1654
|
+
const startedAt = Date.now();
|
|
1655
|
+
setTimeout(() => {
|
|
1656
|
+
if (this.nativePasteAt >= startedAt) {
|
|
1657
|
+
this.diag(`${new Date().toISOString()} paste skipped (native handled)`);
|
|
1658
|
+
return;
|
|
1659
|
+
}
|
|
1660
|
+
this.diag(`${new Date().toISOString()} paste fallback`);
|
|
1661
|
+
void pane.paste();
|
|
1662
|
+
}, PASTE_NATIVE_WAIT_MS);
|
|
1663
|
+
}
|
|
1664
|
+
/**
|
|
1665
|
+
* 이미지 전용 클립보드일 때 터미널 앱에 "네가 클립보드를 직접 읽어라" 키를 흘려보낸다.
|
|
1666
|
+
*
|
|
1667
|
+
* 어떤 키인지는 앱마다 다르다 (2026-08-28 실측):
|
|
1668
|
+
* - Claude Code: ESC v 만 받는다. 리터럴 0x16 은 5회 보내도 이미지가 들어가지 않았다
|
|
1669
|
+
* (`.agentdeck-diag.log` 09:33:47~48 `paste image (ctrl-v)` 5줄, 화면에는 아무 변화 없음).
|
|
1670
|
+
* claude.exe 안의 `ctrl+v ... paste images` 문구는 자기가 키 이벤트를 직접 볼 때의 안내였고,
|
|
1671
|
+
* PTY 로 들어온 0x16 에는 반응하지 않는다.
|
|
1672
|
+
* - Codex: 0x16 을 자기 붙여넣기로 받고, ESC v 는 모른다.
|
|
1673
|
+
* 그래서 탭에서 도는 앱을 보고 갈라 보내고, 못 알아내면 설정값(`imagePasteKey`)을 쓴다.
|
|
1674
|
+
*/
|
|
1675
|
+
async sendImagePasteKey(pane) {
|
|
1676
|
+
const app = await this.detectAgentApp(pane);
|
|
1677
|
+
const configured = this.config.store.agentDeck.imagePasteKey;
|
|
1678
|
+
const mode = app === 'claude' ? 'alt-v'
|
|
1679
|
+
: app === 'codex' ? 'ctrl-v'
|
|
1680
|
+
: configured;
|
|
1681
|
+
const key = mode === 'ctrl-v' ? '' : 'v';
|
|
1682
|
+
this.diag(`${new Date().toISOString()} paste image app=${app} mode=${mode} `
|
|
1683
|
+
+ `bytes=${[...key].map(c => c.charCodeAt(0)).join(',')}`);
|
|
1684
|
+
pane.sendInput(key);
|
|
1685
|
+
}
|
|
1686
|
+
/**
|
|
1687
|
+
* 이 탭에서 도는 에이전트 CLI 가 뭔지 — 이미지 붙여넣기 키가 앱마다 달라서 필요하다.
|
|
1688
|
+
*
|
|
1689
|
+
* 프로세스 트리를 먼저 본다 (`session.getChildProcesses()`). npm 전역 래퍼를 거치면
|
|
1690
|
+
* 커맨드가 node/pwsh 로만 보일 수 있어서, 그때는 탭 제목으로 한 번 더 본다.
|
|
1691
|
+
*/
|
|
1692
|
+
async detectAgentApp(pane) {
|
|
1693
|
+
var _a, _b, _c, _d, _e;
|
|
1694
|
+
try {
|
|
1695
|
+
const procs = (_c = await ((_b = (_a = pane.session) === null || _a === void 0 ? void 0 : _a.getChildProcesses) === null || _b === void 0 ? void 0 : _b.call(_a))) !== null && _c !== void 0 ? _c : [];
|
|
1696
|
+
const names = procs.map(x => { var _a, _b; return `${(_a = x === null || x === void 0 ? void 0 : x.command) !== null && _a !== void 0 ? _a : ''} ${(_b = x === null || x === void 0 ? void 0 : x.name) !== null && _b !== void 0 ? _b : ''}`; }).join(' ').toLowerCase();
|
|
1697
|
+
if (names.includes('codex')) {
|
|
1698
|
+
return 'codex';
|
|
1699
|
+
}
|
|
1700
|
+
if (names.includes('claude')) {
|
|
1701
|
+
return 'claude';
|
|
1702
|
+
}
|
|
1703
|
+
}
|
|
1704
|
+
catch {
|
|
1705
|
+
// 프로세스 트리를 못 읽는 세션(SSH 등) — 제목으로 떨어진다
|
|
1706
|
+
}
|
|
1707
|
+
const title = `${(_d = pane.title) !== null && _d !== void 0 ? _d : ''} ${(_e = pane.customTitle) !== null && _e !== void 0 ? _e : ''}`.toLowerCase();
|
|
1708
|
+
if (title.includes('codex')) {
|
|
1709
|
+
return 'codex';
|
|
1710
|
+
}
|
|
1711
|
+
if (title.includes('claude')) {
|
|
1712
|
+
return 'claude';
|
|
1713
|
+
}
|
|
1714
|
+
return 'unknown';
|
|
1715
|
+
}
|
|
1716
|
+
/**
|
|
1717
|
+
* pane.sendInput 을 한 번 감싸 PTY 로 나간 입력을 진단 로그에 남긴다.
|
|
1718
|
+
*
|
|
1719
|
+
* 붙여넣기 한 번에 텍스트가 두 번 들어가는 증상은 "우리가 두 번 불렀는지" 와
|
|
1720
|
+
* "한 번 불렀는데 PTY 에 두 번 써졌는지" 를 갈라야 원인이 잡힌다. 이 계측이 그 경계다.
|
|
1721
|
+
* `pasteProbeUntil` 창 안에서만 기록하므로 평소 타이핑은 남지 않는다.
|
|
1722
|
+
*/
|
|
1723
|
+
probeSendInput(pane) {
|
|
1724
|
+
if (!pane || pane.__adSendInputProbe || typeof pane.sendInput !== 'function') {
|
|
1725
|
+
return;
|
|
1726
|
+
}
|
|
1727
|
+
const orig = pane.sendInput.bind(pane);
|
|
1728
|
+
pane.__adSendInputProbe = true;
|
|
1729
|
+
pane.sendInput = (data, ...rest) => {
|
|
1730
|
+
if (Date.now() < this.pasteProbeUntil) {
|
|
1731
|
+
const text = typeof data === 'string' ? data : String(data);
|
|
1732
|
+
this.diag(`${new Date().toISOString()} sendInput len=${text.length} `
|
|
1733
|
+
+ `head=${JSON.stringify(text.slice(0, 24))} from=${this.callSite()}`);
|
|
1734
|
+
}
|
|
1735
|
+
return orig(data, ...rest);
|
|
1736
|
+
};
|
|
1737
|
+
}
|
|
1738
|
+
/**
|
|
1739
|
+
* PTY 로 나가는 최종 관문(session.write)을 랩핑한다.
|
|
1740
|
+
*
|
|
1741
|
+
* pane.sendInput 만 보면 그걸 거치지 않는 경로를 놓친다 — 실측(2026-08-28 09:44:03)에서
|
|
1742
|
+
* 우클릭 한 번에 sendInput 은 154바이트 1회뿐인데 화면에는 두 번 들어갔다. 즉 두 번째는
|
|
1743
|
+
* 우리 계측 밖의 경로다. session.write 는 어느 경로든 마지막에 지나가므로 여기서 잡힌다.
|
|
1744
|
+
*/
|
|
1745
|
+
probeWrite(pane) {
|
|
1746
|
+
const session = pane === null || pane === void 0 ? void 0 : pane.session;
|
|
1747
|
+
if (!session || session.__adWriteProbe || typeof session.write !== 'function') {
|
|
1748
|
+
return;
|
|
1749
|
+
}
|
|
1750
|
+
const orig = session.write.bind(session);
|
|
1751
|
+
session.__adWriteProbe = true;
|
|
1752
|
+
session.write = (data, ...rest) => {
|
|
1753
|
+
if (Date.now() < this.pasteProbeUntil) {
|
|
1754
|
+
const text = typeof data === 'string' ? data : String(data);
|
|
1755
|
+
this.diag(`${new Date().toISOString()} write len=${text.length} `
|
|
1756
|
+
+ `head=${JSON.stringify(text.slice(0, 24))} from=${this.callSite()}`);
|
|
1757
|
+
}
|
|
1758
|
+
return orig(data, ...rest);
|
|
1759
|
+
};
|
|
1760
|
+
}
|
|
1761
|
+
/** 호출 지점 몇 프레임 — 두 번째 붙여넣기를 누가 넣는지 보려고 */
|
|
1762
|
+
callSite() {
|
|
1763
|
+
var _a;
|
|
1764
|
+
return ((_a = new Error().stack) !== null && _a !== void 0 ? _a : '').split(String.fromCharCode(10)).slice(3, 7)
|
|
1765
|
+
.map(l => l.trim().replace(/^at\s+/, '')).join(' <- ');
|
|
1766
|
+
}
|
|
1767
|
+
/** 클립보드에 이미지가 있고 텍스트는 비어 있는지 */
|
|
1768
|
+
clipboardHasImageOnly() {
|
|
1769
|
+
const cb = getClipboard();
|
|
1770
|
+
if (!cb) {
|
|
1771
|
+
// Electron clipboard 를 못 쓰는 환경(웹 빌드 등) — 텍스트 붙여넣기로 떨어진다
|
|
1772
|
+
this.diag(`${new Date().toISOString()} clipboard unavailable`);
|
|
1773
|
+
return false;
|
|
1774
|
+
}
|
|
1775
|
+
try {
|
|
1776
|
+
const hasImage = !cb.readImage().isEmpty();
|
|
1777
|
+
const text = cb.readText();
|
|
1778
|
+
this.diag(`${new Date().toISOString()} clipboard hasImage=${hasImage} textLen=${text.length}`);
|
|
1779
|
+
return hasImage && text.trim() === '';
|
|
1780
|
+
}
|
|
1781
|
+
catch (err) {
|
|
1782
|
+
this.diag(`${new Date().toISOString()} clipboard read failed: ${err}`);
|
|
1783
|
+
return false;
|
|
1784
|
+
}
|
|
1785
|
+
}
|
|
1786
|
+
/** 분할된 탭이면 래퍼가 아니라 실제 포커스된 pane 을 돌려준다 */
|
|
1787
|
+
focusedPane() {
|
|
1788
|
+
var _a;
|
|
1789
|
+
const active = this.app.activeTab;
|
|
1790
|
+
if (!active) {
|
|
1791
|
+
return null;
|
|
1792
|
+
}
|
|
1793
|
+
return typeof active.getFocusedTab === 'function'
|
|
1794
|
+
? ((_a = active.getFocusedTab()) !== null && _a !== void 0 ? _a : active)
|
|
1795
|
+
: active;
|
|
1796
|
+
}
|
|
1797
|
+
/**
|
|
1798
|
+
* 줄바꿈 핫키에 Shift+Enter 를 보정해 넣는다.
|
|
1799
|
+
*
|
|
1800
|
+
* config.yaml 에 이미 `agentdeck-newline` 이 저장돼 있으면 ConfigProvider 의 defaults 는
|
|
1801
|
+
* 무시되므로(저장값이 이긴다) 기동 시 코드로 채워 넣어야 한다 — ensurePasteHotkey 와 같은 이유.
|
|
1802
|
+
*/
|
|
1803
|
+
ensureNewlineHotkey() {
|
|
1804
|
+
const hotkeys = this.config.store.hotkeys;
|
|
1805
|
+
if (!hotkeys || !Array.isArray(hotkeys['agentdeck-newline'])) {
|
|
1806
|
+
return;
|
|
1807
|
+
}
|
|
1808
|
+
const list = hotkeys['agentdeck-newline'];
|
|
1809
|
+
if (list.includes('Shift-Enter')) {
|
|
1810
|
+
return;
|
|
1811
|
+
}
|
|
1812
|
+
hotkeys['agentdeck-newline'] = [...list, 'Shift-Enter'];
|
|
1813
|
+
this.config.save();
|
|
1814
|
+
}
|
|
1815
|
+
// ---------- 우클릭 ----------
|
|
1816
|
+
/**
|
|
1817
|
+
* 우클릭 처리를 agentdeck 이 가져온다.
|
|
1818
|
+
*
|
|
1819
|
+
* 증상: 우클릭 한 번에 클립보드가 두 번 들어갔다.
|
|
1820
|
+
* 원인: Tabby 의 우클릭 처리는 터미널 host 엘리먼트에 건 버블 단계 mouseup 리스너인데
|
|
1821
|
+
* (`tabby-terminal/dist/index.js:40972` 에서 등록, `detach()` 는 이 리스너를 떼지 않는다)
|
|
1822
|
+
* 프론트엔드가 같은 host 에 다시 attach 되면 리스너가 겹쳐 붙어 mouseEvent$ 가 한 클릭에
|
|
1823
|
+
* 두 번 발화한다. 그러면 `handleRightMouseUp` 이 두 번 돌아 paste() 도 두 번 나간다.
|
|
1824
|
+
*
|
|
1825
|
+
* 해결: Tabby 쪽 처리를 끄고(terminal.rightClick = 'off') document 캡처 단계에서
|
|
1826
|
+
* 우리가 한 번만 처리한다. 캡처는 host 리스너보다 먼저 돌기 때문에 몇 개가 겹쳐 붙어 있든
|
|
1827
|
+
* stopPropagation 으로 전부 차단된다. 동작 자체는 Tabby 의 'clipboard' 모드와 동일하게 —
|
|
1828
|
+
* 선택이 있으면 복사, 없으면 붙여넣기, 길게 누르면 컨텍스트 메뉴 — 유지한다.
|
|
1829
|
+
*/
|
|
1830
|
+
claimRightClick() {
|
|
1831
|
+
if (!this.config.store.agentDeck.claimRightClick) {
|
|
1832
|
+
return;
|
|
1833
|
+
}
|
|
1834
|
+
// Tabby 자체 처리를 꺼서 리스너가 겹쳐도 아무 일이 없게 만든다 (이중 안전장치)
|
|
1835
|
+
if (this.config.store.terminal && this.config.store.terminal.rightClick !== 'off') {
|
|
1836
|
+
this.config.store.terminal.rightClick = 'off';
|
|
1837
|
+
this.config.save();
|
|
1838
|
+
}
|
|
1839
|
+
document.addEventListener('mousedown', ev => {
|
|
1840
|
+
if (ev.button !== 2 || !this.inTerminalArea(ev)) {
|
|
1841
|
+
return;
|
|
1842
|
+
}
|
|
1843
|
+
this.rightDownAt = Date.now();
|
|
1844
|
+
// mouseup 만 막으면 mousedown 이 xterm 까지 내려가 마우스 리포트가 PTY 로 나간다.
|
|
1845
|
+
// Claude Code 처럼 마우스 트래킹을 켠 앱은 그 우클릭 리포트(ESC [ < 2 ; x ; y M)를
|
|
1846
|
+
// 자기 붙여넣기 신호로 받아 스스로 클립보드를 읽는다 — 우리 paste 와 합쳐 두 번 붙는다
|
|
1847
|
+
// (2026-08-28 실측: .agentdeck-diag.log 09:49:08.616 에 "[<2;172;35M" 전송,
|
|
1848
|
+
// 플러그인 write 는 클릭당 1회뿐인데 화면에는 2회 들어감).
|
|
1849
|
+
ev.preventDefault();
|
|
1850
|
+
ev.stopPropagation();
|
|
1851
|
+
}, { capture: true });
|
|
1852
|
+
// 우클릭에 딸려오는 contextmenu 도 캡처에서 끊는다 (메뉴는 길게 누를 때 우리가 직접 띄운다)
|
|
1853
|
+
document.addEventListener('contextmenu', ev => {
|
|
1854
|
+
if (this.inTerminalArea(ev)) {
|
|
1855
|
+
ev.preventDefault();
|
|
1856
|
+
ev.stopPropagation();
|
|
1857
|
+
}
|
|
1858
|
+
}, { capture: true });
|
|
1859
|
+
document.addEventListener('mouseup', ev => {
|
|
1860
|
+
if (ev.button !== 2 || !this.inTerminalArea(ev)) {
|
|
1861
|
+
return;
|
|
1862
|
+
}
|
|
1863
|
+
ev.preventDefault();
|
|
1864
|
+
ev.stopPropagation();
|
|
1865
|
+
// 같은 물리 클릭이 두 번 들어오면(리스너 중복) 뒤엣것은 버린다
|
|
1866
|
+
const now = Date.now();
|
|
1867
|
+
if (now - this.lastRightHandledAt < 150) {
|
|
1868
|
+
return;
|
|
1869
|
+
}
|
|
1870
|
+
this.lastRightHandledAt = now;
|
|
1871
|
+
const held = now - this.rightDownAt;
|
|
1872
|
+
void this.handleRightClick(ev, held);
|
|
1873
|
+
}, { capture: true });
|
|
1874
|
+
}
|
|
1875
|
+
/** 사이드바가 아니라 터미널 뷰포트 안에서 일어난 이벤트인지 */
|
|
1876
|
+
inTerminalArea(ev) {
|
|
1877
|
+
const target = ev.target;
|
|
1878
|
+
if (!target || typeof target.closest !== 'function') {
|
|
1879
|
+
return false;
|
|
1880
|
+
}
|
|
1881
|
+
return !!target.closest('.content.main') && !target.closest(`#${SIDEBAR_ID}`);
|
|
1882
|
+
}
|
|
1883
|
+
async handleRightClick(ev, heldMs) {
|
|
1884
|
+
var _a, _b;
|
|
1885
|
+
const pane = this.focusedPane();
|
|
1886
|
+
if (!pane) {
|
|
1887
|
+
return;
|
|
1888
|
+
}
|
|
1889
|
+
const menuMs = (_a = this.config.store.agentDeck.rightClickMenuMs) !== null && _a !== void 0 ? _a : 250;
|
|
1890
|
+
if (heldMs >= menuMs) {
|
|
1891
|
+
if (typeof pane.buildContextMenu === 'function' && pane.platform) {
|
|
1892
|
+
pane.platform.popupContextMenu(await pane.buildContextMenu(), ev);
|
|
1893
|
+
}
|
|
1894
|
+
return;
|
|
1895
|
+
}
|
|
1896
|
+
const selection = typeof ((_b = pane.frontend) === null || _b === void 0 ? void 0 : _b.getSelection) === 'function'
|
|
1897
|
+
? pane.frontend.getSelection()
|
|
1898
|
+
: '';
|
|
1899
|
+
if (selection) {
|
|
1900
|
+
pane.frontend.copySelection();
|
|
1901
|
+
pane.frontend.clearSelection();
|
|
1902
|
+
}
|
|
1903
|
+
else {
|
|
1904
|
+
// 우클릭 붙여넣기도 Ctrl+V 와 같은 경로를 타게 한다 (이미지면 Alt+V 위임)
|
|
1905
|
+
this.doPaste('rightclick');
|
|
1906
|
+
}
|
|
1907
|
+
}
|
|
1908
|
+
/**
|
|
1909
|
+
* 포커스된 pane 에 0x0A(LF) 를 보낸다 — Ctrl+J 가 보내는 것과 같은 바이트다.
|
|
1910
|
+
* Claude Code 는 이걸 줄바꿈으로 받고, Enter(0x0D) 는 전송으로 받는다.
|
|
1911
|
+
* Enter 를 눌러 줄을 넘기지 않으려는 것이므로 CR 을 섞으면 안 된다.
|
|
1912
|
+
*/
|
|
1913
|
+
sendNewline() {
|
|
1914
|
+
const pane = this.focusedPane();
|
|
1915
|
+
if (!pane) {
|
|
1916
|
+
return;
|
|
1917
|
+
}
|
|
1918
|
+
if (typeof pane.sendInput === 'function') {
|
|
1919
|
+
pane.sendInput('\n');
|
|
1920
|
+
}
|
|
1921
|
+
}
|
|
1922
|
+
// ---------- 레이아웃 ----------
|
|
1923
|
+
get enabled() {
|
|
1924
|
+
return this.config.store.agentDeck.enabled;
|
|
1925
|
+
}
|
|
1926
|
+
applyEnabled() {
|
|
1927
|
+
document.body.classList.toggle(BODY_CLASS, this.enabled);
|
|
1928
|
+
if (this.sidebar) {
|
|
1929
|
+
this.sidebar.style.display = this.enabled ? '' : 'none';
|
|
1930
|
+
}
|
|
1931
|
+
if (!this.enabled && this.mainEl) {
|
|
1932
|
+
this.mainEl.style.removeProperty('flex');
|
|
1933
|
+
this.mainEl.style.removeProperty('width');
|
|
1934
|
+
this.mainEl.style.removeProperty('max-width');
|
|
1935
|
+
this.mainEl.style.removeProperty('margin-left');
|
|
1936
|
+
// 폭을 되돌렸으니 메모도 무효화하고 터미널에 새 폭을 알린다
|
|
1937
|
+
this.lastTermW = -1;
|
|
1938
|
+
this.lastSidebarW = -1;
|
|
1939
|
+
this.scheduleRefit();
|
|
1940
|
+
}
|
|
1941
|
+
}
|
|
1942
|
+
/**
|
|
1943
|
+
* 터미널/사이드바 배경에 알파를 넣어 tabby-background 의 배경 이미지가 비치게 한다.
|
|
1944
|
+
*
|
|
1945
|
+
* 배경 이미지는 `.content-tab-active::before` 로 **모든 내용 뒤에** 깔린다. 그래서
|
|
1946
|
+
* 그 위를 덮는 두 면이 불투명하면 이미지는 어디에도 보이지 않는다 —
|
|
1947
|
+
* (1) 터미널: xterm 이 컬러스킴 background 색으로 셀을 직접 칠한다 (CSS 로는 못 뚫는다)
|
|
1948
|
+
* (2) 사이드바 / .window: 우리가 넣은 불투명 배경
|
|
1949
|
+
* (1) 은 Tabby 가 xterm 을 `allowTransparency: true` 로 만들기 때문에 색에 알파만 넣으면 된다.
|
|
1950
|
+
* config.yaml 을 손으로 고쳐도 Tabby 가 종료할 때 메모리 값으로 덮어써 되돌아가므로,
|
|
1951
|
+
* ensurePasteHotkey 와 같은 이유로 여기서 코드로 보정한다.
|
|
1952
|
+
*/
|
|
1953
|
+
applyOpacity() {
|
|
1954
|
+
var _a;
|
|
1955
|
+
const cfg = this.config.store.agentDeck;
|
|
1956
|
+
const termPct = clampPct(cfg.terminalOpacity);
|
|
1957
|
+
const sidePct = clampPct(cfg.sidebarOpacity);
|
|
1958
|
+
// 사이드바 / 창 여백 — CSS 변수로 내려보낸다 (styles.scss 가 폴백을 들고 있다)
|
|
1959
|
+
const root = document.documentElement;
|
|
1960
|
+
if (sidePct >= 100) {
|
|
1961
|
+
root.style.removeProperty('--ad-panel-bg');
|
|
1962
|
+
}
|
|
1963
|
+
else {
|
|
1964
|
+
root.style.setProperty('--ad-panel-bg', `rgba(30, 33, 39, ${sidePct / 100})`);
|
|
1965
|
+
}
|
|
1966
|
+
// 터미널 — 컬러스킴 배경색에 알파를 붙인다
|
|
1967
|
+
const scheme = (_a = this.config.store.terminal) === null || _a === void 0 ? void 0 : _a.colorScheme;
|
|
1968
|
+
if (!(scheme === null || scheme === void 0 ? void 0 : scheme.background)) {
|
|
1969
|
+
return;
|
|
1970
|
+
}
|
|
1971
|
+
const next = withAlpha(scheme.background, termPct);
|
|
1972
|
+
// 컬러스킴 기반 배경을 쓰도록 못 박는다 — 테마 배경으로 떨어지면 알파가 무시된다
|
|
1973
|
+
const wantMode = termPct < 100 ? 'colorScheme' : this.config.store.terminal.background;
|
|
1974
|
+
if (next === scheme.background && wantMode === this.config.store.terminal.background) {
|
|
1975
|
+
// 이미 적용돼 있다. 여기서 save 를 부르면 changed$ 가 다시 우리를 부른다
|
|
1976
|
+
return;
|
|
1977
|
+
}
|
|
1978
|
+
scheme.background = next;
|
|
1979
|
+
this.config.store.terminal.background = wantMode;
|
|
1980
|
+
this.config.save();
|
|
1981
|
+
}
|
|
1982
|
+
/** 창 높이에서 터미널 폭을 역산해 .content.main 을 고정폭으로 만든다 */
|
|
1983
|
+
relayout() {
|
|
1984
|
+
if (!this.enabled || !this.windowEl) {
|
|
1985
|
+
this.diagOnce('relayout-skip-enabled', `relayout skipped enabled=${this.enabled} windowEl=${!!this.windowEl}`);
|
|
1986
|
+
return;
|
|
1987
|
+
}
|
|
1988
|
+
// appRoot 가 늦게 렌더되는 경우가 있어 매번 다시 찾는다
|
|
1989
|
+
if (!this.mainEl) {
|
|
1990
|
+
this.mainEl = document.querySelector('.content.main');
|
|
1991
|
+
}
|
|
1992
|
+
if (!this.mainEl) {
|
|
1993
|
+
this.diagOnce('relayout-skip-mainel', 'relayout skipped mainEl=null');
|
|
1994
|
+
return;
|
|
1995
|
+
}
|
|
1996
|
+
const cfg = this.config.store.agentDeck;
|
|
1997
|
+
const availW = this.windowEl.clientWidth;
|
|
1998
|
+
const availH = this.mainEl.clientHeight || this.windowEl.clientHeight;
|
|
1999
|
+
if (!availW || !availH) {
|
|
2000
|
+
return;
|
|
2001
|
+
}
|
|
2002
|
+
const ratio = (cfg.aspectW || 4) / (cfg.aspectH || 3);
|
|
2003
|
+
const minSidebar = cfg.sidebarMin || 200;
|
|
2004
|
+
const maxSidebar = Math.max(cfg.sidebarMax || 560, minSidebar);
|
|
2005
|
+
// 1) 4:3 을 지킨다는 가정에서 터미널 폭을 높이로 역산한다
|
|
2006
|
+
let termW = Math.round(availH * ratio);
|
|
2007
|
+
termW = Math.min(termW, availW - minSidebar);
|
|
2008
|
+
termW = Math.max(termW, 320);
|
|
2009
|
+
// 2) 사이드바는 min~max 로 클램프한다 (max 는 config agentDeck.sidebarMax 로 사용자가 올릴 수 있다)
|
|
2010
|
+
const sidebarW = Math.min(Math.max(availW - termW, minSidebar), maxSidebar);
|
|
2011
|
+
// 3) 그래도 폭이 남으면 4:3 을 포기하고 터미널이 흡수한다 — 오른쪽에 빈 띠를 남기지 않는다
|
|
2012
|
+
// (2026-08-28 실측: 창2000 - 터미널1460 - 사이드바430 = 110px 잔여가 빈 띠로 보였다)
|
|
2013
|
+
termW = Math.max(availW - sidebarW, 320);
|
|
2014
|
+
// 값이 그대로면 DOM 을 건드리지 않는다 — relayout 은 탭/상태가 바뀔 때마다 불리므로
|
|
2015
|
+
// 매번 스타일을 다시 쓰면 xterm 이 불필요하게 재측정한다
|
|
2016
|
+
if (termW === this.lastTermW && sidebarW === this.lastSidebarW) {
|
|
2017
|
+
return;
|
|
2018
|
+
}
|
|
2019
|
+
const prevTermW = this.lastTermW;
|
|
2020
|
+
this.lastTermW = termW;
|
|
2021
|
+
this.lastSidebarW = sidebarW;
|
|
2022
|
+
// 기동 직후에 폭이 실제로 바뀌었다면, 그 전에 그려진 TUI 첫 화면은 옛 폭 기준이다.
|
|
2023
|
+
// 예약해둔 흔들기가 이미 지나갔을 수도 있으므로 여기서 다시 한 번 흔들어준다
|
|
2024
|
+
// (창 복원→최대화를 손으로 하던 것과 같은 효과 — kickPtySize 주석 참고).
|
|
2025
|
+
if (this.setupAt && Date.now() - this.setupAt < KICK_REARM_MS) {
|
|
2026
|
+
this.diag(`${new Date().toISOString()} rearm kick termW ${prevTermW}->${termW} panes=${this.startupPanes.length}`);
|
|
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';
|
|
2039
|
+
if (this.sidebar) {
|
|
2040
|
+
this.sidebar.style.width = sidebarW + 'px';
|
|
2041
|
+
}
|
|
2042
|
+
this.scheduleRefit();
|
|
2043
|
+
}
|
|
2044
|
+
/**
|
|
2045
|
+
* 폭을 바꿨으면 터미널에 반드시 알려야 한다.
|
|
2046
|
+
*
|
|
2047
|
+
* Tabby 의 xterm 프론트엔드는 창(OS 창) 리사이즈에 맞춰 재측정하지만,
|
|
2048
|
+
* 우리처럼 CSS 로만 `.content.main` 폭을 바꾸면 그 경로를 타지 않는다.
|
|
2049
|
+
* 그러면 xterm 의 열 수는 넓어졌는데 pty 는 옛 폭 그대로라, Claude Code 같은
|
|
2050
|
+
* 전체화면 TUI 가 화면보다 좁게 그리고 오른쪽에 지워지지 않은 잔상이 남는다
|
|
2051
|
+
* (2026-08-28 실측: 입력창 구분선이 화면 중간에서 끊기고 옛 statusline 이 우측에 박혀 있었다.
|
|
2052
|
+
* 창을 복원→최대화로 흔들면 OS 리사이즈 경로를 타서 즉시 정상 복구됐다).
|
|
2053
|
+
*
|
|
2054
|
+
* fitAddon.fit() 이 xterm 의 onResize 를 발화시키고, Tabby 가 그걸 받아
|
|
2055
|
+
* session.resize() 로 pty 까지 내려보낸다. 값이 같으면 xterm 이 이벤트를 내지 않으므로
|
|
2056
|
+
* 여분 호출은 무해하다.
|
|
2057
|
+
*/
|
|
2058
|
+
scheduleRefit() {
|
|
2059
|
+
if (this.refitQueued) {
|
|
2060
|
+
return;
|
|
2061
|
+
}
|
|
2062
|
+
this.refitQueued = true;
|
|
2063
|
+
// 레이아웃이 실제로 반영된 뒤에 재야 한다 — 한 프레임으로는 아직 옛 폭이 잡힌다
|
|
2064
|
+
requestAnimationFrame(() => requestAnimationFrame(() => {
|
|
2065
|
+
this.refitQueued = false;
|
|
2066
|
+
this.refitTerminals();
|
|
2067
|
+
}));
|
|
2068
|
+
// attach() 가 아직 진행 중이면 위 fit 이 무시될 수 있다. fit 은 크기가 같으면
|
|
2069
|
+
// 아무 일도 안 하므로 한 번 더 늦게 때려도 무해하다
|
|
2070
|
+
setTimeout(() => this.refitTerminals(), 500);
|
|
2071
|
+
}
|
|
2072
|
+
refitTerminals() {
|
|
2073
|
+
var _a, _b;
|
|
2074
|
+
for (const tab of this.app.tabs) {
|
|
2075
|
+
const anyTab = tab;
|
|
2076
|
+
const panes = typeof anyTab.getAllTabs === 'function'
|
|
2077
|
+
? anyTab.getAllTabs()
|
|
2078
|
+
: [tab];
|
|
2079
|
+
for (const pane of panes) {
|
|
2080
|
+
const frontend = pane.frontend;
|
|
2081
|
+
const fitAddon = frontend === null || frontend === void 0 ? void 0 : frontend.fitAddon;
|
|
2082
|
+
if (!fitAddon || typeof fitAddon.fit !== 'function') {
|
|
2083
|
+
continue;
|
|
2084
|
+
}
|
|
2085
|
+
// 숨겨진 탭은 컨테이너 폭이 0 이라 fit 하면 1열짜리로 줄어든다 — 건너뛴다.
|
|
2086
|
+
// 그 탭은 다시 선택될 때 Tabby 가 알아서 재측정한다
|
|
2087
|
+
const host = (_b = (_a = frontend.xterm) === null || _a === void 0 ? void 0 : _a.element) === null || _b === void 0 ? void 0 : _b.parentElement;
|
|
2088
|
+
if (!host || !host.clientWidth || !host.clientHeight) {
|
|
2089
|
+
continue;
|
|
2090
|
+
}
|
|
2091
|
+
try {
|
|
2092
|
+
fitAddon.fit();
|
|
2093
|
+
}
|
|
2094
|
+
catch {
|
|
2095
|
+
// 아직 붙지 않은 프론트엔드 — 다음 relayout 때 다시 잡힌다
|
|
2096
|
+
}
|
|
2097
|
+
this.syncPtySize(pane, frontend);
|
|
2098
|
+
}
|
|
2099
|
+
}
|
|
2100
|
+
}
|
|
2101
|
+
/**
|
|
2102
|
+
* pty 크기를 xterm 이 실제로 그리고 있는 크기에 강제로 맞춘다.
|
|
2103
|
+
*
|
|
2104
|
+
* `fitAddon.fit()` 은 xterm 의 열/행이 이미 맞으면 onResize 를 내지 않는다. 그런데 새 탭은
|
|
2105
|
+
* xterm 은 우리가 줄여 놓은 폭으로 제대로 열리는데 **세션(pty)만** 생성 시점의 옛 폭으로 남는
|
|
2106
|
+
* 경우가 있다. 이때 fit() 은 no-op 이라 pty 가 영영 안 고쳐지고, TUI 가 처음 화면을 다시
|
|
2107
|
+
* 그리는 순간(줄바꿈 등) 오른쪽에 옛 폭 잔상이 남는다 — 창 복원→최대화로 흔들면 OS 리사이즈
|
|
2108
|
+
* 경로를 타서 그제야 맞는다(2026-08-28 실측, 새 탭에서 재현).
|
|
2109
|
+
*
|
|
2110
|
+
* 그래서 fit() 뒤에 pty 로 직접 한 번 더 내려보낸다. 같은 값이면 pty 쪽에서 무시되므로 무해하다.
|
|
2111
|
+
*/
|
|
2112
|
+
syncPtySize(pane, frontend) {
|
|
2113
|
+
var _a, _b;
|
|
2114
|
+
const cols = (_a = frontend === null || frontend === void 0 ? void 0 : frontend.xterm) === null || _a === void 0 ? void 0 : _a.cols;
|
|
2115
|
+
const rows = (_b = frontend === null || frontend === void 0 ? void 0 : frontend.xterm) === null || _b === void 0 ? void 0 : _b.rows;
|
|
2116
|
+
if (!cols || !rows) {
|
|
2117
|
+
return;
|
|
2118
|
+
}
|
|
2119
|
+
const anyPane = pane;
|
|
2120
|
+
const session = anyPane.session;
|
|
2121
|
+
if (!session || !session.open || typeof session.resize !== 'function') {
|
|
2122
|
+
return;
|
|
2123
|
+
}
|
|
2124
|
+
try {
|
|
2125
|
+
session.resize(cols, rows);
|
|
2126
|
+
anyPane.size = { columns: cols, rows };
|
|
2127
|
+
}
|
|
2128
|
+
catch {
|
|
2129
|
+
// 세션이 막 닫혔다 — 다음 refit 때 다시 잡힌다
|
|
2130
|
+
}
|
|
2131
|
+
}
|
|
2132
|
+
// ---------- 사이드바 DOM ----------
|
|
2133
|
+
buildSidebar() {
|
|
2134
|
+
const el = document.createElement('div');
|
|
2135
|
+
el.id = SIDEBAR_ID;
|
|
2136
|
+
el.innerHTML = [
|
|
2137
|
+
'<div class="ad-head">',
|
|
2138
|
+
' <span class="ad-head-title">AGENT DECK</span>',
|
|
2139
|
+
' <span class="ad-head-count"></span>',
|
|
2140
|
+
'</div>',
|
|
2141
|
+
'<div class="ad-list"></div>',
|
|
2142
|
+
'<div class="ad-foot">',
|
|
2143
|
+
' <button class="ad-btn ad-new" title="새 탭">+ 새 탭</button>',
|
|
2144
|
+
' <button class="ad-btn ad-settings" title="설정">설정</button>',
|
|
2145
|
+
'</div>',
|
|
2146
|
+
].join('\n');
|
|
2147
|
+
// 순정 탭바보다 뒤(오른쪽)에 놓는다 — 왼쪽에 두면 세로로 길어 허전하다는 피드백(2026-08-28)
|
|
2148
|
+
this.windowEl.appendChild(el);
|
|
2149
|
+
this.sidebar = el;
|
|
2150
|
+
this.listEl = el.querySelector('.ad-list');
|
|
2151
|
+
el.querySelector('.ad-new').addEventListener('click', () => {
|
|
2152
|
+
this.zone.run(() => { void this.openNewTab(); });
|
|
2153
|
+
});
|
|
2154
|
+
el.querySelector('.ad-settings').addEventListener('click', () => {
|
|
2155
|
+
this.zone.run(() => this.openSettings());
|
|
2156
|
+
});
|
|
2157
|
+
}
|
|
2158
|
+
/** 설정에 지정된 기본 프로필(기본값 agentdeck:root)로 새 탭을 연다 */
|
|
2159
|
+
async openNewTab() {
|
|
2160
|
+
var _a;
|
|
2161
|
+
const wanted = this.config.store.terminal.profile;
|
|
2162
|
+
const profiles = await this.profiles.getProfiles();
|
|
2163
|
+
const profile = (_a = profiles.find(p => p.id === wanted)) !== null && _a !== void 0 ? _a : profiles[0];
|
|
2164
|
+
if (profile) {
|
|
2165
|
+
await this.profiles.openNewTabForProfile(profile);
|
|
2166
|
+
}
|
|
2167
|
+
}
|
|
2168
|
+
openSettings() {
|
|
2169
|
+
this.app.openNewTabRaw({ type: tabby_settings__WEBPACK_IMPORTED_MODULE_2__.SettingsTabComponent });
|
|
2170
|
+
}
|
|
2171
|
+
/**
|
|
2172
|
+
* 사이드바 다시 그리기를 한 번으로 모은다.
|
|
2173
|
+
*
|
|
2174
|
+
* rAF 만 쓰면 **창이 가려졌을 때 상태가 늦게 반영된다.** Chromium 은 창이 hidden
|
|
2175
|
+
* (최소화·완전 가려짐) 이 되면 rAF 를 아예 멈추고 타이머도 스로틀한다. 그러면
|
|
2176
|
+
* 훅이 보낸 done 은 메모리에 즉시 반영됐는데 화면만 옛 상태로 남아 있다가,
|
|
2177
|
+
* 1초 tick(그것도 스로틀되면 더 늦게)이 와야 그려진다 — "작업중이 한참 뒤에야 완료로 바뀐다"
|
|
2178
|
+
* 의 정체가 이것이다(2026-08-28). 시작 쪽이 빨라 보이는 건 그때는 사용자가 방금
|
|
2179
|
+
* 창에 입력해서 창이 떠 있기 때문일 뿐이다.
|
|
2180
|
+
*
|
|
2181
|
+
* 그래서 rAF 와 짧은 타이머를 같이 걸고 **먼저 오는 쪽이 그린다**. 창이 떠 있으면
|
|
2182
|
+
* 지금까지와 똑같이 다음 프레임에, 가려져 있으면 타이머가 대신 그린다.
|
|
2183
|
+
*/
|
|
2184
|
+
scheduleRender() {
|
|
2185
|
+
if (this.renderQueued) {
|
|
2186
|
+
return;
|
|
2187
|
+
}
|
|
2188
|
+
this.renderQueued = true;
|
|
2189
|
+
const run = () => {
|
|
2190
|
+
if (!this.renderQueued) {
|
|
2191
|
+
return;
|
|
2192
|
+
}
|
|
2193
|
+
this.renderQueued = false;
|
|
2194
|
+
if (this.renderTimer) {
|
|
2195
|
+
clearTimeout(this.renderTimer);
|
|
2196
|
+
this.renderTimer = null;
|
|
2197
|
+
}
|
|
2198
|
+
this.render();
|
|
2199
|
+
this.relayout();
|
|
2200
|
+
};
|
|
2201
|
+
requestAnimationFrame(run);
|
|
2202
|
+
this.renderTimer = setTimeout(run, 50);
|
|
2203
|
+
}
|
|
2204
|
+
/**
|
|
2205
|
+
* 창이 가려져 있어도 타이머·rAF 가 계속 돌게 한다.
|
|
2206
|
+
*
|
|
2207
|
+
* 위 scheduleRender 의 타이머 폴백은 "타이머가 제때 돈다" 를 전제로 하는데,
|
|
2208
|
+
* Chromium 은 hidden 페이지의 타이머를 1초로 묶고 5분 넘게 가려져 있으면
|
|
2209
|
+
* **분당 1회**까지 떨어뜨린다. 그 상태에서는 폴백조차 늦다.
|
|
2210
|
+
* Electron 의 backgroundThrottling 을 꺼서 이 계층을 통째로 없앤다.
|
|
2211
|
+
*/
|
|
2212
|
+
disableBackgroundThrottling() {
|
|
2213
|
+
var _a, _b, _c, _d, _e;
|
|
2214
|
+
if (!this.config.store.agentDeck.keepRenderingWhenHidden) {
|
|
2215
|
+
return;
|
|
2216
|
+
}
|
|
2217
|
+
try {
|
|
2218
|
+
const req = (_a = window.require) !== null && _a !== void 0 ? _a : globalThis.require;
|
|
2219
|
+
const win = (_c = (_b = req === null || req === void 0 ? void 0 : req('@electron/remote')) === null || _b === void 0 ? void 0 : _b.getCurrentWindow) === null || _c === void 0 ? void 0 : _c.call(_b);
|
|
2220
|
+
(_e = (_d = win === null || win === void 0 ? void 0 : win.webContents) === null || _d === void 0 ? void 0 : _d.setBackgroundThrottling) === null || _e === void 0 ? void 0 : _e.call(_d, false);
|
|
2221
|
+
}
|
|
2222
|
+
catch {
|
|
2223
|
+
// remote 를 못 잡아도 위 타이머 폴백은 살아 있다 (조금 늦을 뿐)
|
|
2224
|
+
}
|
|
2225
|
+
}
|
|
2226
|
+
tick() {
|
|
2227
|
+
if (!this.enabled) {
|
|
2228
|
+
return;
|
|
2229
|
+
}
|
|
2230
|
+
let dirty = false;
|
|
2231
|
+
const idleAfter = this.config.store.agentDeck.idleAfterMs || 2500;
|
|
2232
|
+
for (const tab of this.app.tabs) {
|
|
2233
|
+
if (this.status.tickIdle(tab, idleAfter)) {
|
|
2234
|
+
dirty = true;
|
|
2235
|
+
}
|
|
2236
|
+
}
|
|
2237
|
+
if (dirty || this.config.store.agentDeck.showElapsed) {
|
|
2238
|
+
this.render();
|
|
2239
|
+
}
|
|
2240
|
+
}
|
|
2241
|
+
render() {
|
|
2242
|
+
if (!this.listEl || !this.enabled || this.editing) {
|
|
2243
|
+
return;
|
|
2244
|
+
}
|
|
2245
|
+
const tabs = this.app.tabs;
|
|
2246
|
+
const countEl = this.sidebar.querySelector('.ad-head-count');
|
|
2247
|
+
countEl.textContent = String(tabs.length);
|
|
2248
|
+
this.listEl.innerHTML = '';
|
|
2249
|
+
for (const tab of tabs) {
|
|
2250
|
+
this.listEl.appendChild(this.renderTab(tab));
|
|
2251
|
+
}
|
|
2252
|
+
}
|
|
2253
|
+
renderTab(tab) {
|
|
2254
|
+
const st = this.status.get(tab);
|
|
2255
|
+
const style = _api__WEBPACK_IMPORTED_MODULE_8__.STATUS_STYLES[st.status];
|
|
2256
|
+
const active = tab === this.app.activeTab;
|
|
2257
|
+
const row = document.createElement('div');
|
|
2258
|
+
row.className = active ? 'ad-tab active' : 'ad-tab';
|
|
2259
|
+
row.style.setProperty('--ad-color', style.color);
|
|
2260
|
+
// [AD:...] 표식은 상태로 이미 반영했으니 제목에서는 지운다
|
|
2261
|
+
const tabTitle = (0,_detect__WEBPACK_IMPORTED_MODULE_7__.stripTitleMarker)(tab.customTitle || tab.title || '');
|
|
2262
|
+
// 제목 줄은 라벨(= 지금 무슨 작업 중인지)이 있으면 그쪽을 쓴다.
|
|
2263
|
+
// Claude Code 는 세션 시작 때 붙인 콘솔 제목을 계속 들고 있어 다음 작업으로 넘어가도
|
|
2264
|
+
// 안 바뀐다(2026-08-28 실측 — 이 판정 때문에 제목을 라벨 원천으로 쓰려던 계획을 접었다).
|
|
2265
|
+
// 제목만 보여주면 사이드바가 옛 작업 이름에, 최악엔 셸이 붙인 엉뚱한 문자열에 박혀 있게 된다.
|
|
2266
|
+
const useLabel = this.config.store.agentDeck.labelAsTitle && !!st.label;
|
|
2267
|
+
const title = (useLabel ? st.label : tabTitle) || tabTitle || '(제목 없음)';
|
|
2268
|
+
const elapsed = this.config.store.agentDeck.showElapsed
|
|
2269
|
+
? this.formatElapsed(Date.now() - st.since)
|
|
2270
|
+
: '';
|
|
2271
|
+
row.innerHTML = [
|
|
2272
|
+
'<div class="ad-bar"></div>',
|
|
2273
|
+
'<div class="ad-body">',
|
|
2274
|
+
' <div class="ad-title"></div>',
|
|
2275
|
+
' <div class="ad-meta">',
|
|
2276
|
+
' <span class="ad-badge"></span>',
|
|
2277
|
+
' <span class="ad-label"></span>',
|
|
2278
|
+
' <span class="ad-elapsed"></span>',
|
|
2279
|
+
' </div>',
|
|
2280
|
+
'</div>',
|
|
2281
|
+
'<button class="ad-close" title="탭 닫기">×</button>',
|
|
2282
|
+
].join('\n');
|
|
2283
|
+
// textContent 로만 넣는다 — 탭 제목에 어떤 문자가 와도 마크업으로 해석되지 않게
|
|
2284
|
+
row.querySelector('.ad-title').textContent = title;
|
|
2285
|
+
// 제목 줄로 승격된 라벨을 아래에 한 번 더 쓰지 않는다
|
|
2286
|
+
row.querySelector('.ad-label').textContent = st.label === title ? '' : st.label;
|
|
2287
|
+
row.querySelector('.ad-elapsed').textContent = elapsed;
|
|
2288
|
+
const badge = row.querySelector('.ad-badge');
|
|
2289
|
+
badge.textContent = style.icon + ' ' + style.label;
|
|
2290
|
+
if (st.pinned) {
|
|
2291
|
+
badge.classList.add('pinned');
|
|
2292
|
+
}
|
|
2293
|
+
row.addEventListener('click', ev => {
|
|
2294
|
+
if (ev.target.closest('.ad-close')) {
|
|
2295
|
+
return;
|
|
2296
|
+
}
|
|
2297
|
+
this.zone.run(() => this.app.selectTab(tab));
|
|
2298
|
+
});
|
|
2299
|
+
row.addEventListener('dblclick', ev => {
|
|
2300
|
+
ev.preventDefault();
|
|
2301
|
+
this.startLabelEdit(row, tab);
|
|
2302
|
+
});
|
|
2303
|
+
row.addEventListener('contextmenu', ev => {
|
|
2304
|
+
ev.preventDefault();
|
|
2305
|
+
this.showStatusMenu(ev, tab);
|
|
2306
|
+
});
|
|
2307
|
+
row.querySelector('.ad-close').addEventListener('click', ev => {
|
|
2308
|
+
ev.stopPropagation();
|
|
2309
|
+
this.zone.run(() => { void this.app.closeTab(tab, true); });
|
|
2310
|
+
});
|
|
2311
|
+
return row;
|
|
2312
|
+
}
|
|
2313
|
+
/** 더블클릭 -> 작업 이름 인라인 편집 */
|
|
2314
|
+
startLabelEdit(row, tab) {
|
|
2315
|
+
this.editing = tab;
|
|
2316
|
+
const holder = row.querySelector('.ad-label');
|
|
2317
|
+
const input = document.createElement('input');
|
|
2318
|
+
input.className = 'ad-label-input';
|
|
2319
|
+
input.value = this.status.get(tab).label;
|
|
2320
|
+
input.placeholder = '작업 이름';
|
|
2321
|
+
holder.replaceWith(input);
|
|
2322
|
+
input.focus();
|
|
2323
|
+
input.select();
|
|
2324
|
+
const finish = (commit) => {
|
|
2325
|
+
if (this.editing !== tab) {
|
|
2326
|
+
return;
|
|
2327
|
+
}
|
|
2328
|
+
this.editing = null;
|
|
2329
|
+
if (commit) {
|
|
2330
|
+
this.status.setLabel(tab, input.value.trim());
|
|
2331
|
+
}
|
|
2332
|
+
this.render();
|
|
2333
|
+
};
|
|
2334
|
+
// 터미널로 키가 새지 않도록 이벤트를 여기서 끊는다
|
|
2335
|
+
input.addEventListener('keydown', ev => {
|
|
2336
|
+
ev.stopPropagation();
|
|
2337
|
+
if (ev.key === 'Enter') {
|
|
2338
|
+
finish(true);
|
|
2339
|
+
}
|
|
2340
|
+
if (ev.key === 'Escape') {
|
|
2341
|
+
finish(false);
|
|
2342
|
+
}
|
|
2343
|
+
});
|
|
2344
|
+
input.addEventListener('blur', () => finish(true));
|
|
2345
|
+
}
|
|
2346
|
+
/** 우클릭 -> 상태 직접 지정 */
|
|
2347
|
+
showStatusMenu(ev, tab) {
|
|
2348
|
+
const old = document.querySelector('.ad-menu');
|
|
2349
|
+
if (old) {
|
|
2350
|
+
old.remove();
|
|
2351
|
+
}
|
|
2352
|
+
const menu = document.createElement('div');
|
|
2353
|
+
menu.className = 'ad-menu';
|
|
2354
|
+
const statuses = ['running', 'waiting', 'done', 'error', 'idle'];
|
|
2355
|
+
const items = statuses.map(s => {
|
|
2356
|
+
const label = _api__WEBPACK_IMPORTED_MODULE_8__.STATUS_STYLES[s].icon + ' ' + _api__WEBPACK_IMPORTED_MODULE_8__.STATUS_STYLES[s].label;
|
|
2357
|
+
const action = () => this.status.setManual(tab, s);
|
|
2358
|
+
return [label, action];
|
|
2359
|
+
});
|
|
2360
|
+
items.push(['↺ 자동 감지로', () => this.status.unpin(tab)]);
|
|
2361
|
+
for (const [label, action] of items) {
|
|
2362
|
+
const item = document.createElement('div');
|
|
2363
|
+
item.className = 'ad-menu-item';
|
|
2364
|
+
item.textContent = label;
|
|
2365
|
+
item.addEventListener('click', () => {
|
|
2366
|
+
action();
|
|
2367
|
+
menu.remove();
|
|
2368
|
+
});
|
|
2369
|
+
menu.appendChild(item);
|
|
2370
|
+
}
|
|
2371
|
+
document.body.appendChild(menu);
|
|
2372
|
+
menu.style.left = ev.clientX + 'px';
|
|
2373
|
+
menu.style.top = Math.min(ev.clientY, window.innerHeight - menu.offsetHeight - 8) + 'px';
|
|
2374
|
+
const close = (e) => {
|
|
2375
|
+
if (!menu.contains(e.target)) {
|
|
2376
|
+
menu.remove();
|
|
2377
|
+
document.removeEventListener('mousedown', close);
|
|
2378
|
+
}
|
|
2379
|
+
};
|
|
2380
|
+
setTimeout(() => document.addEventListener('mousedown', close), 0);
|
|
2381
|
+
}
|
|
2382
|
+
formatElapsed(ms) {
|
|
2383
|
+
const s = Math.floor(ms / 1000);
|
|
2384
|
+
if (s < 60) {
|
|
2385
|
+
return s + '초';
|
|
2386
|
+
}
|
|
2387
|
+
const m = Math.floor(s / 60);
|
|
2388
|
+
if (m < 60) {
|
|
2389
|
+
return m + '분';
|
|
2390
|
+
}
|
|
2391
|
+
return Math.floor(m / 60) + '시간 ' + (m % 60) + '분';
|
|
2392
|
+
}
|
|
2393
|
+
};
|
|
2394
|
+
AgentDeckService = __decorate([
|
|
2395
|
+
(0,_angular_core__WEBPACK_IMPORTED_MODULE_0__.Injectable)({ providedIn: 'root' }),
|
|
2396
|
+
__metadata("design:paramtypes", [tabby_core__WEBPACK_IMPORTED_MODULE_1__.AppService,
|
|
2397
|
+
tabby_core__WEBPACK_IMPORTED_MODULE_1__.ConfigService,
|
|
2398
|
+
_status_service__WEBPACK_IMPORTED_MODULE_6__.WorkStatusService,
|
|
2399
|
+
tabby_core__WEBPACK_IMPORTED_MODULE_1__.HotkeysService,
|
|
2400
|
+
tabby_core__WEBPACK_IMPORTED_MODULE_1__.ProfilesService,
|
|
2401
|
+
tabby_core__WEBPACK_IMPORTED_MODULE_1__.HostWindowService,
|
|
2402
|
+
_angular_core__WEBPACK_IMPORTED_MODULE_0__.NgZone])
|
|
2403
|
+
], AgentDeckService);
|
|
2404
|
+
|
|
2405
|
+
|
|
2406
|
+
|
|
2407
|
+
/***/ },
|
|
2408
|
+
|
|
2409
|
+
/***/ "./src/decorator.ts"
|
|
2410
|
+
/*!**************************!*\
|
|
2411
|
+
!*** ./src/decorator.ts ***!
|
|
2412
|
+
\**************************/
|
|
2413
|
+
(__unused_webpack_module, __webpack_exports__, __webpack_require__) {
|
|
2414
|
+
|
|
2415
|
+
__webpack_require__.r(__webpack_exports__);
|
|
2416
|
+
/* harmony export */ __webpack_require__.d(__webpack_exports__, {
|
|
2417
|
+
/* harmony export */ AgentDeckDecorator: () => (/* binding */ AgentDeckDecorator)
|
|
2418
|
+
/* harmony export */ });
|
|
2419
|
+
/* harmony import */ var _angular_core__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! @angular/core */ "@angular/core");
|
|
2420
|
+
/* harmony import */ var _angular_core__WEBPACK_IMPORTED_MODULE_0___default = /*#__PURE__*/__webpack_require__.n(_angular_core__WEBPACK_IMPORTED_MODULE_0__);
|
|
2421
|
+
/* harmony import */ var tabby_core__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(/*! tabby-core */ "tabby-core");
|
|
2422
|
+
/* harmony import */ var tabby_core__WEBPACK_IMPORTED_MODULE_1___default = /*#__PURE__*/__webpack_require__.n(tabby_core__WEBPACK_IMPORTED_MODULE_1__);
|
|
2423
|
+
/* harmony import */ var tabby_terminal__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(/*! tabby-terminal */ "tabby-terminal");
|
|
2424
|
+
/* harmony import */ var tabby_terminal__WEBPACK_IMPORTED_MODULE_2___default = /*#__PURE__*/__webpack_require__.n(tabby_terminal__WEBPACK_IMPORTED_MODULE_2__);
|
|
2425
|
+
/* harmony import */ var _status_service__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(/*! ./status.service */ "./src/status.service.ts");
|
|
2426
|
+
/* harmony import */ var _detect__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(/*! ./detect */ "./src/detect.ts");
|
|
2427
|
+
var __decorate = (undefined && undefined.__decorate) || function (decorators, target, key, desc) {
|
|
2428
|
+
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
2429
|
+
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
|
2430
|
+
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;
|
|
2431
|
+
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
2432
|
+
};
|
|
2433
|
+
var __metadata = (undefined && undefined.__metadata) || function (k, v) {
|
|
2434
|
+
if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
|
|
2435
|
+
};
|
|
2436
|
+
|
|
2437
|
+
|
|
2438
|
+
|
|
2439
|
+
|
|
2440
|
+
|
|
2441
|
+
/**
|
|
2442
|
+
* PTY 출력을 훔쳐보며 탭 상태를 추론한다.
|
|
2443
|
+
*
|
|
2444
|
+
* Tabby 는 프론트엔드(xterm)가 첫 리사이즈를 보고할 때 decorator 를 붙이므로
|
|
2445
|
+
* (tabby-terminal/src/api/baseTerminalTab.component.ts:386) 그 시점을 놓친 탭은
|
|
2446
|
+
* 여기로 들어오지 않는다. 그 빈틈은 AgentDeckService 의 세션 워처가 메운다.
|
|
2447
|
+
*/
|
|
2448
|
+
let AgentDeckDecorator = class AgentDeckDecorator extends tabby_terminal__WEBPACK_IMPORTED_MODULE_2__.TerminalDecorator {
|
|
2449
|
+
constructor(status, config, app) {
|
|
2450
|
+
super();
|
|
2451
|
+
this.status = status;
|
|
2452
|
+
this.config = config;
|
|
2453
|
+
this.app = app;
|
|
2454
|
+
}
|
|
2455
|
+
/**
|
|
2456
|
+
* 상태를 기록할 키를 사이드바가 보는 것과 같은 탭으로 맞춘다.
|
|
2457
|
+
*
|
|
2458
|
+
* Tabby 는 모든 탭을 SplitTabComponent 로 감싸므로 app.tabs 에 들어있는 것은
|
|
2459
|
+
* 그 래퍼이고, decorator 가 받는 것은 그 안의 터미널 탭이다. 정규화하지 않으면
|
|
2460
|
+
* 상태를 자식에 쓰고 사이드바는 부모를 읽어 영영 매칭되지 않는다.
|
|
2461
|
+
* 분할된 pane 여러 개는 하나의 탭 상태를 공유한다.
|
|
2462
|
+
*/
|
|
2463
|
+
rootOf(tab) {
|
|
2464
|
+
var _a;
|
|
2465
|
+
return (_a = this.app.getParentTab(tab)) !== null && _a !== void 0 ? _a : tab;
|
|
2466
|
+
}
|
|
2467
|
+
attach(tab) {
|
|
2468
|
+
this.status.debug.attached++;
|
|
2469
|
+
if (tab.session) {
|
|
2470
|
+
this.attachToSession(tab, tab.session);
|
|
2471
|
+
}
|
|
2472
|
+
this.subscribeUntilDetached(tab, tab.sessionChanged$.subscribe(session => {
|
|
2473
|
+
if (session) {
|
|
2474
|
+
this.attachToSession(tab, session);
|
|
2475
|
+
}
|
|
2476
|
+
}));
|
|
2477
|
+
}
|
|
2478
|
+
detach(tab) {
|
|
2479
|
+
const root = this.rootOf(tab);
|
|
2480
|
+
super.detach(tab);
|
|
2481
|
+
(0,_detect__WEBPACK_IMPORTED_MODULE_4__.forgetOutputBuffer)(tab);
|
|
2482
|
+
// 분할 pane 중 하나만 닫힌 경우라면 탭 상태는 남겨둔다
|
|
2483
|
+
if (root === tab || !this.app.tabs.includes(root)) {
|
|
2484
|
+
this.status.forget(root);
|
|
2485
|
+
}
|
|
2486
|
+
}
|
|
2487
|
+
attachToSession(tab, session) {
|
|
2488
|
+
this.subscribeUntilDetached(tab, session.output$.subscribe(data => {
|
|
2489
|
+
;(0,_detect__WEBPACK_IMPORTED_MODULE_4__.applyOutput)(this.status, this.rootOf(tab), tab, data, this.config.store.agentDeck.autoDetect);
|
|
2490
|
+
}));
|
|
2491
|
+
}
|
|
2492
|
+
};
|
|
2493
|
+
AgentDeckDecorator = __decorate([
|
|
2494
|
+
(0,_angular_core__WEBPACK_IMPORTED_MODULE_0__.Injectable)(),
|
|
2495
|
+
__metadata("design:paramtypes", [_status_service__WEBPACK_IMPORTED_MODULE_3__.WorkStatusService,
|
|
2496
|
+
tabby_core__WEBPACK_IMPORTED_MODULE_1__.ConfigService,
|
|
2497
|
+
tabby_core__WEBPACK_IMPORTED_MODULE_1__.AppService])
|
|
2498
|
+
], AgentDeckDecorator);
|
|
2499
|
+
|
|
2500
|
+
|
|
2501
|
+
|
|
2502
|
+
/***/ },
|
|
2503
|
+
|
|
2504
|
+
/***/ "./src/detect.ts"
|
|
2505
|
+
/*!***********************!*\
|
|
2506
|
+
!*** ./src/detect.ts ***!
|
|
2507
|
+
\***********************/
|
|
2508
|
+
(__unused_webpack_module, __webpack_exports__, __webpack_require__) {
|
|
2509
|
+
|
|
2510
|
+
__webpack_require__.r(__webpack_exports__);
|
|
2511
|
+
/* harmony export */ __webpack_require__.d(__webpack_exports__, {
|
|
2512
|
+
/* harmony export */ applyOutput: () => (/* binding */ applyOutput),
|
|
2513
|
+
/* harmony export */ applyTitle: () => (/* binding */ applyTitle),
|
|
2514
|
+
/* harmony export */ forgetOutputBuffer: () => (/* binding */ forgetOutputBuffer),
|
|
2515
|
+
/* harmony export */ stripTitleMarker: () => (/* binding */ stripTitleMarker)
|
|
2516
|
+
/* harmony export */ });
|
|
2517
|
+
/** 훅이 상태를 직접 통보하는 커스텀 OSC — ESC ] 1337 ; AgentDeck=<status>;<label> BEL */
|
|
2518
|
+
const OSC_RE = /\x1b\]1337;AgentDeck=([a-z]+)(?:;([^\x07\x1b]*))?(?:\x07|\x1b\\)/gi;
|
|
2519
|
+
/** Claude Code 등 TUI 가 사용자 입력을 기다릴 때 나타나는 패턴 */
|
|
2520
|
+
const WAITING_RE = [
|
|
2521
|
+
/Do you want to\b/i,
|
|
2522
|
+
/❯\s*1\.\s*Yes/,
|
|
2523
|
+
/\(y\/n\)/i,
|
|
2524
|
+
/\[y\/N\]/i,
|
|
2525
|
+
/Press Enter to continue/i,
|
|
2526
|
+
];
|
|
2527
|
+
const VALID = ['idle', 'running', 'waiting', 'done', 'error'];
|
|
2528
|
+
/** OSC 가 청크 경계에 걸리는 것을 막기 위한 잔여 버퍼 */
|
|
2529
|
+
const tails = new WeakMap();
|
|
2530
|
+
/**
|
|
2531
|
+
* PTY 출력 한 조각을 보고 탭 상태를 갱신한다.
|
|
2532
|
+
*
|
|
2533
|
+
* 우선순위: OSC 통보 > 대기 패턴 > "출력이 있다 = 진행중".
|
|
2534
|
+
* idle 로의 복귀는 사이드바 타이머(tickIdle)가 담당한다.
|
|
2535
|
+
*
|
|
2536
|
+
* decorator 와 deck 의 세션 워처 양쪽에서 호출된다 — 같은 조각을 두 번 먹어도
|
|
2537
|
+
* 결과가 같도록 상태 설정은 모두 멱등하게 만들어 두었다.
|
|
2538
|
+
*/
|
|
2539
|
+
function applyOutput(status, root, owner, data, autoDetect) {
|
|
2540
|
+
var _a;
|
|
2541
|
+
status.debug.outputs++;
|
|
2542
|
+
status.markOutput(root);
|
|
2543
|
+
const buf = ((_a = tails.get(owner)) !== null && _a !== void 0 ? _a : '') + data;
|
|
2544
|
+
let matched = false;
|
|
2545
|
+
let m;
|
|
2546
|
+
OSC_RE.lastIndex = 0;
|
|
2547
|
+
while ((m = OSC_RE.exec(buf)) !== null) {
|
|
2548
|
+
const s = m[1].toLowerCase();
|
|
2549
|
+
if (VALID.includes(s)) {
|
|
2550
|
+
status.setManual(root, s, m[2]);
|
|
2551
|
+
status.debug.oscHits++;
|
|
2552
|
+
matched = true;
|
|
2553
|
+
}
|
|
2554
|
+
}
|
|
2555
|
+
// 미완성 OSC 가 남아있을 수 있으므로 꼬리만 보관
|
|
2556
|
+
tails.set(owner, buf.slice(-256));
|
|
2557
|
+
if (matched || !autoDetect) {
|
|
2558
|
+
return;
|
|
2559
|
+
}
|
|
2560
|
+
if (WAITING_RE.some(re => re.test(data))) {
|
|
2561
|
+
status.setAuto(root, 'waiting');
|
|
2562
|
+
return;
|
|
2563
|
+
}
|
|
2564
|
+
status.setAuto(root, 'running');
|
|
2565
|
+
}
|
|
2566
|
+
function forgetOutputBuffer(owner) {
|
|
2567
|
+
tails.delete(owner);
|
|
2568
|
+
}
|
|
2569
|
+
/**
|
|
2570
|
+
* 탭 제목에 실려 온 상태 표식 — `[AD:running] 결제 버그 수정` 형태.
|
|
2571
|
+
*
|
|
2572
|
+
* Windows 에서 이 경로를 쓰는 이유: ConPTY 는 자기가 모르는 OSC 를 삼켜서
|
|
2573
|
+
* 커스텀 이스케이프가 PTY 스트림까지 오지 못한다(2026-08-28 실측 — 출력에 남은 건
|
|
2574
|
+
* 입력 에코뿐이었다). 반면 콘솔 제목은 ConPTY 가 API 로 변환해 전달하므로 확실히 통과한다.
|
|
2575
|
+
*/
|
|
2576
|
+
const TITLE_RE = /\[AD:([a-z]+)\]\s*(.*)$/i;
|
|
2577
|
+
/**
|
|
2578
|
+
* Claude Code 가 작업 중일 때 제목에 넣는 표식.
|
|
2579
|
+
* 별 모양과 원형 스피너 두 계열을 모두 쓴다 (2026-08-28 실측: "◐ WindowTitle 설정").
|
|
2580
|
+
*/
|
|
2581
|
+
const BUSY_MARKS = ['✳', '✻', '✽', '✶', '✢', '◐', '◓', '◑', '◒'];
|
|
2582
|
+
function stripTitleMarker(title) {
|
|
2583
|
+
return title.replace(/\[AD:[a-z]+\]\s*/i, '');
|
|
2584
|
+
}
|
|
2585
|
+
function applyTitle(status, root, title, autoDetect) {
|
|
2586
|
+
const m = TITLE_RE.exec(title);
|
|
2587
|
+
if (m) {
|
|
2588
|
+
const s = m[1].toLowerCase();
|
|
2589
|
+
if (VALID.includes(s)) {
|
|
2590
|
+
status.setManual(root, s, m[2].trim());
|
|
2591
|
+
status.debug.titleHits++;
|
|
2592
|
+
return;
|
|
2593
|
+
}
|
|
2594
|
+
}
|
|
2595
|
+
// 명시 표식이 없으면 Claude Code 의 작업중 표식이라도 읽는다
|
|
2596
|
+
if (autoDetect && BUSY_MARKS.some(mark => title.includes(mark))) {
|
|
2597
|
+
status.setAuto(root, 'running');
|
|
2598
|
+
}
|
|
2599
|
+
}
|
|
2600
|
+
|
|
2601
|
+
|
|
2602
|
+
/***/ },
|
|
2603
|
+
|
|
2604
|
+
/***/ "./src/hotkeys.ts"
|
|
2605
|
+
/*!************************!*\
|
|
2606
|
+
!*** ./src/hotkeys.ts ***!
|
|
2607
|
+
\************************/
|
|
2608
|
+
(__unused_webpack_module, __webpack_exports__, __webpack_require__) {
|
|
2609
|
+
|
|
2610
|
+
__webpack_require__.r(__webpack_exports__);
|
|
2611
|
+
/* harmony export */ __webpack_require__.d(__webpack_exports__, {
|
|
2612
|
+
/* harmony export */ AgentDeckHotkeyProvider: () => (/* binding */ AgentDeckHotkeyProvider)
|
|
2613
|
+
/* 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
|
+
/**
|
|
2627
|
+
* 핫키 "선언"만 한다.
|
|
2628
|
+
*
|
|
2629
|
+
* 여기서 HotkeysService 를 주입하면 안 된다 — HotkeysService 자신이
|
|
2630
|
+
* HotkeyProvider 목록을 주입받으므로(tabby-core/src/services/hotkeys.service.ts:85)
|
|
2631
|
+
* 순환 의존이 되어 부분 초기화된 인스턴스가 넘어온다.
|
|
2632
|
+
* 실제 핫키 처리는 AgentDeckService 에서 구독한다.
|
|
2633
|
+
*/
|
|
2634
|
+
let AgentDeckHotkeyProvider = class AgentDeckHotkeyProvider extends tabby_core__WEBPACK_IMPORTED_MODULE_1__.HotkeyProvider {
|
|
2635
|
+
async provide() {
|
|
2636
|
+
return [
|
|
2637
|
+
{
|
|
2638
|
+
id: 'agentdeck-toggle',
|
|
2639
|
+
name: 'AgentDeck 사이드바 / 4:3 전환',
|
|
2640
|
+
},
|
|
2641
|
+
{
|
|
2642
|
+
id: 'agentdeck-newline',
|
|
2643
|
+
name: 'AgentDeck 줄바꿈 (Ctrl+Enter / Shift+Enter, 0x0A 전송)',
|
|
2644
|
+
},
|
|
2645
|
+
{
|
|
2646
|
+
id: 'agentdeck-paste',
|
|
2647
|
+
name: 'AgentDeck 붙여넣기 (텍스트는 그대로, 이미지는 Alt+V 로 위임)',
|
|
2648
|
+
},
|
|
2649
|
+
];
|
|
2650
|
+
}
|
|
2651
|
+
};
|
|
2652
|
+
AgentDeckHotkeyProvider = __decorate([
|
|
2653
|
+
(0,_angular_core__WEBPACK_IMPORTED_MODULE_0__.Injectable)()
|
|
2654
|
+
], AgentDeckHotkeyProvider);
|
|
2655
|
+
|
|
2656
|
+
|
|
2657
|
+
|
|
2658
|
+
/***/ },
|
|
2659
|
+
|
|
2660
|
+
/***/ "./src/notify.service.ts"
|
|
2661
|
+
/*!*******************************!*\
|
|
2662
|
+
!*** ./src/notify.service.ts ***!
|
|
2663
|
+
\*******************************/
|
|
2664
|
+
(__unused_webpack_module, __webpack_exports__, __webpack_require__) {
|
|
2665
|
+
|
|
2666
|
+
__webpack_require__.r(__webpack_exports__);
|
|
2667
|
+
/* harmony export */ __webpack_require__.d(__webpack_exports__, {
|
|
2668
|
+
/* harmony export */ WorkNotifyService: () => (/* binding */ WorkNotifyService)
|
|
2669
|
+
/* harmony export */ });
|
|
2670
|
+
/* harmony import */ var _angular_core__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! @angular/core */ "@angular/core");
|
|
2671
|
+
/* harmony import */ var _angular_core__WEBPACK_IMPORTED_MODULE_0___default = /*#__PURE__*/__webpack_require__.n(_angular_core__WEBPACK_IMPORTED_MODULE_0__);
|
|
2672
|
+
/* harmony import */ var tabby_core__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(/*! tabby-core */ "tabby-core");
|
|
2673
|
+
/* harmony import */ var tabby_core__WEBPACK_IMPORTED_MODULE_1___default = /*#__PURE__*/__webpack_require__.n(tabby_core__WEBPACK_IMPORTED_MODULE_1__);
|
|
2674
|
+
/* harmony import */ var fs__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(/*! fs */ "fs");
|
|
2675
|
+
/* harmony import */ var fs__WEBPACK_IMPORTED_MODULE_2___default = /*#__PURE__*/__webpack_require__.n(fs__WEBPACK_IMPORTED_MODULE_2__);
|
|
2676
|
+
/* harmony import */ var net__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(/*! net */ "net");
|
|
2677
|
+
/* harmony import */ var net__WEBPACK_IMPORTED_MODULE_3___default = /*#__PURE__*/__webpack_require__.n(net__WEBPACK_IMPORTED_MODULE_3__);
|
|
2678
|
+
/* harmony import */ var os__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(/*! os */ "os");
|
|
2679
|
+
/* harmony import */ var os__WEBPACK_IMPORTED_MODULE_4___default = /*#__PURE__*/__webpack_require__.n(os__WEBPACK_IMPORTED_MODULE_4__);
|
|
2680
|
+
/* harmony import */ var path__WEBPACK_IMPORTED_MODULE_5__ = __webpack_require__(/*! path */ "path");
|
|
2681
|
+
/* harmony import */ var path__WEBPACK_IMPORTED_MODULE_5___default = /*#__PURE__*/__webpack_require__.n(path__WEBPACK_IMPORTED_MODULE_5__);
|
|
2682
|
+
/* harmony import */ var _status_service__WEBPACK_IMPORTED_MODULE_6__ = __webpack_require__(/*! ./status.service */ "./src/status.service.ts");
|
|
2683
|
+
var __decorate = (undefined && undefined.__decorate) || function (decorators, target, key, desc) {
|
|
2684
|
+
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
2685
|
+
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
|
2686
|
+
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;
|
|
2687
|
+
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
2688
|
+
};
|
|
2689
|
+
var __metadata = (undefined && undefined.__metadata) || function (k, v) {
|
|
2690
|
+
if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
|
|
2691
|
+
};
|
|
2692
|
+
|
|
2693
|
+
|
|
2694
|
+
|
|
2695
|
+
|
|
2696
|
+
|
|
2697
|
+
|
|
2698
|
+
|
|
2699
|
+
/**
|
|
2700
|
+
* 외부 도구 -> 사이드바 상태 채널. **기본은 꺼져 있다** (`notifyChannel`).
|
|
2701
|
+
*
|
|
2702
|
+
* 라벨은 Enter 시점 입력창에서, 상태는 출력 패턴에서 플러그인이 스스로 얻는 것이 기본 경로다
|
|
2703
|
+
* (claimEnterLabel / detect.ts). 설정 파일을 손봐야 도는 훅은 배포판에서 기대할 수 없어서
|
|
2704
|
+
* 라벨 원천에서 내렸다. 이 채널은 외부 도구가 상태를 직접 보고하고 싶을 때만 켠다.
|
|
2705
|
+
*
|
|
2706
|
+
* 왜 파일인가 — 콘솔 제목 방식(`hooks/agentdeck-status.ps1`)은 **훅에서는 통하지 않는다**.
|
|
2707
|
+
* Claude Code 는 훅을 파이프 stdio 로 띄우기 때문에 그 자식 프로세스는 터미널의 콘솔에
|
|
2708
|
+
* 붙어 있지 않고, 제목을 바꿔도 pty 까지 오지 않는다 (2026-08-28 실측: 훅으로
|
|
2709
|
+
* `[AD:waiting] ...` 을 걸어도 사이드바가 그대로였다). 셸 안에서 직접 실행할 때는
|
|
2710
|
+
* 제목 방식이 여전히 유효하므로 그 경로는 남겨 둔다.
|
|
2711
|
+
*
|
|
2712
|
+
* 전달 경로는 두 가닥이다.
|
|
2713
|
+
*
|
|
2714
|
+
* 1. **TCP (기본)** — 이쪽이 `127.0.0.1` 임의 포트로 듣고, 실제 포트를
|
|
2715
|
+
* `%LOCALAPPDATA%\tabby-agentdeck\port` 에 적어 둔다. 훅은 그 포트로 JSON 한 줄을
|
|
2716
|
+
* 보내고 끊는다. 폴링 지연이 없고 상태 파일도 남지 않는다.
|
|
2717
|
+
* 2. **파일 (폴백)** — TCP 연결이 안 되면(Tabby 미기동, 포트 파일 없음, 방화벽)
|
|
2718
|
+
* 훅이 `...\status\<sessionId>.json` 에 쓰고 이쪽이 400ms 마다 읽는다.
|
|
2719
|
+
*
|
|
2720
|
+
* 두 경로 모두 `apply()` 로 모이고, `applied` 의 ts 대조가 중복 적용을 막으므로
|
|
2721
|
+
* 같은 보고가 양쪽으로 들어와도 결과는 같다.
|
|
2722
|
+
*
|
|
2723
|
+
* 세션 -> 탭 매칭: 어떤 Claude 세션이 **처음** 보고할 때의 활성 탭에 묶는다.
|
|
2724
|
+
* 사용자가 프롬프트를 넣은 탭이 곧 활성 탭이므로 이 규칙이면 탭이 여러 개여도 맞다.
|
|
2725
|
+
* 한 번 묶으면 그 뒤로는 활성 탭이 바뀌어도 같은 탭을 따라간다.
|
|
2726
|
+
*/
|
|
2727
|
+
let WorkNotifyService = class WorkNotifyService {
|
|
2728
|
+
constructor(app, config, status) {
|
|
2729
|
+
this.app = app;
|
|
2730
|
+
this.config = config;
|
|
2731
|
+
this.status = status;
|
|
2732
|
+
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
|
+
this.dir = path__WEBPACK_IMPORTED_MODULE_5__.join(this.root, 'status');
|
|
2734
|
+
/** 훅이 접속할 포트를 알려주는 파일 */
|
|
2735
|
+
this.portFile = path__WEBPACK_IMPORTED_MODULE_5__.join(this.root, 'port');
|
|
2736
|
+
this.server = null;
|
|
2737
|
+
this.sessionTab = new Map();
|
|
2738
|
+
/** 역방향 — 한 탭을 두 세션이 나눠 쓰지 못하게 막는다 */
|
|
2739
|
+
this.tabSession = new Map();
|
|
2740
|
+
/** 이미 적용한 (sessionId -> ts) — 같은 파일을 반복 적용하지 않는다 */
|
|
2741
|
+
this.applied = new Map();
|
|
2742
|
+
this.timer = null;
|
|
2743
|
+
/** 폴링을 시작한 시각 — 이보다 오래된 파일은 지난 실행의 잔재다 */
|
|
2744
|
+
this.startedAt = 0;
|
|
2745
|
+
}
|
|
2746
|
+
init() {
|
|
2747
|
+
this.app.ready$.subscribe(() => {
|
|
2748
|
+
// 기본은 꺼져 있다 — 라벨은 Enter 시점 입력창에서, 상태는 출력 패턴에서 코드가 스스로
|
|
2749
|
+
// 얻는다(claimEnterLabel / detect.ts). 바깥에서 밀어 넣을 것이 없는데 채널만 열어 두면
|
|
2750
|
+
// 400ms 폴링과 포트 점유가 헛돈다. 외부 도구가 직접 보고하고 싶을 때만 켠다
|
|
2751
|
+
if (!this.config.store.agentDeck.notifyChannel) {
|
|
2752
|
+
return;
|
|
2753
|
+
}
|
|
2754
|
+
this.startedAt = Date.now();
|
|
2755
|
+
this.listen();
|
|
2756
|
+
this.timer = setInterval(() => this.poll(), 400);
|
|
2757
|
+
});
|
|
2758
|
+
// 탭이 닫히면 점유를 풀어야 그 자리를 다음 세션이 쓸 수 있다
|
|
2759
|
+
this.app.tabClosed$.subscribe(tab => {
|
|
2760
|
+
const sid = this.tabSession.get(tab);
|
|
2761
|
+
if (sid) {
|
|
2762
|
+
this.tabSession.delete(tab);
|
|
2763
|
+
this.sessionTab.delete(sid);
|
|
2764
|
+
}
|
|
2765
|
+
});
|
|
2766
|
+
}
|
|
2767
|
+
/** 진단용 */
|
|
2768
|
+
get debugDir() { return this.dir; }
|
|
2769
|
+
/** 진단용 — 듣고 있는 포트 (0 = TCP 채널이 안 떴다) */
|
|
2770
|
+
get debugPort() {
|
|
2771
|
+
var _a;
|
|
2772
|
+
const addr = (_a = this.server) === null || _a === void 0 ? void 0 : _a.address();
|
|
2773
|
+
return addr && typeof addr === 'object' ? addr.port : 0;
|
|
2774
|
+
}
|
|
2775
|
+
/**
|
|
2776
|
+
* 훅이 상태를 바로 밀어 넣을 TCP 채널을 연다.
|
|
2777
|
+
*
|
|
2778
|
+
* 기본은 `agentDeck.notifyPort` 고정 포트다 — 번호가 늘 같아야 훅이나 진단 도구가
|
|
2779
|
+
* 바로 붙을 수 있다. Windows 임시 포트 범위(49152~65535) 아래로 잡아 두어야
|
|
2780
|
+
* OS 가 그 번호를 다른 프로세스에 먼저 내주는 일이 없다.
|
|
2781
|
+
*
|
|
2782
|
+
* 그 번호가 이미 물려 있으면(Tabby 두 개, 다른 프로그램) 0 으로 다시 시도한다 —
|
|
2783
|
+
* OS 가 빈 포트를 골라 준다. 어느 쪽이든 실제 번호는 port 파일에 적어 두므로
|
|
2784
|
+
* 훅은 고정/임의를 구분할 필요가 없다. 둘 다 실패해도 파일 폴백이 살아 있다.
|
|
2785
|
+
*/
|
|
2786
|
+
listen() {
|
|
2787
|
+
const server = net__WEBPACK_IMPORTED_MODULE_3__.createServer(socket => {
|
|
2788
|
+
let buf = '';
|
|
2789
|
+
socket.setEncoding('utf8');
|
|
2790
|
+
socket.on('data', chunk => {
|
|
2791
|
+
buf += chunk;
|
|
2792
|
+
// 훅이 여러 건을 몰아 보낼 수도 있으니 줄 단위로 끊어 처리한다
|
|
2793
|
+
let nl;
|
|
2794
|
+
while ((nl = buf.indexOf('\n')) >= 0) {
|
|
2795
|
+
const line = buf.slice(0, nl);
|
|
2796
|
+
buf = buf.slice(nl + 1);
|
|
2797
|
+
this.accept(line);
|
|
2798
|
+
}
|
|
2799
|
+
});
|
|
2800
|
+
// 개행 없이 끊는 훅도 받아 준다
|
|
2801
|
+
socket.on('end', () => this.accept(buf));
|
|
2802
|
+
socket.on('error', () => { });
|
|
2803
|
+
});
|
|
2804
|
+
let fellBack = false;
|
|
2805
|
+
server.on('error', () => {
|
|
2806
|
+
if (!fellBack) {
|
|
2807
|
+
// 고정 포트가 물려 있다 — OS 가 골라 주는 번호로 한 번 더 시도한다
|
|
2808
|
+
fellBack = true;
|
|
2809
|
+
server.listen(0, '127.0.0.1');
|
|
2810
|
+
return;
|
|
2811
|
+
}
|
|
2812
|
+
this.server = null;
|
|
2813
|
+
// 지난 실행의 포트 번호를 남겨 두면 훅이 그 번호로 접속을 시도한다.
|
|
2814
|
+
// 그 사이 다른 프로그램이 같은 번호를 잡았다면 엉뚱한 곳으로 JSON 을 쏘게 되므로 지운다
|
|
2815
|
+
try {
|
|
2816
|
+
fs__WEBPACK_IMPORTED_MODULE_2__.unlinkSync(this.portFile);
|
|
2817
|
+
}
|
|
2818
|
+
catch {
|
|
2819
|
+
// 이미 없으면 그만이다
|
|
2820
|
+
}
|
|
2821
|
+
});
|
|
2822
|
+
server.on('listening', () => {
|
|
2823
|
+
this.server = server;
|
|
2824
|
+
try {
|
|
2825
|
+
fs__WEBPACK_IMPORTED_MODULE_2__.mkdirSync(this.root, { recursive: true });
|
|
2826
|
+
fs__WEBPACK_IMPORTED_MODULE_2__.writeFileSync(this.portFile, String(this.debugPort), 'utf8');
|
|
2827
|
+
}
|
|
2828
|
+
catch {
|
|
2829
|
+
// 포트를 못 알리면 훅은 파일 경로로 떨어진다
|
|
2830
|
+
}
|
|
2831
|
+
});
|
|
2832
|
+
server.listen(this.config.store.agentDeck.notifyPort || 0, '127.0.0.1');
|
|
2833
|
+
}
|
|
2834
|
+
/** TCP 로 들어온 JSON 한 줄 */
|
|
2835
|
+
accept(line) {
|
|
2836
|
+
const text = line.trim();
|
|
2837
|
+
if (!text) {
|
|
2838
|
+
return;
|
|
2839
|
+
}
|
|
2840
|
+
try {
|
|
2841
|
+
this.apply(JSON.parse(text));
|
|
2842
|
+
}
|
|
2843
|
+
catch {
|
|
2844
|
+
// 깨진 줄은 버린다 — 다음 보고가 곧 온다
|
|
2845
|
+
}
|
|
2846
|
+
}
|
|
2847
|
+
poll() {
|
|
2848
|
+
let names;
|
|
2849
|
+
try {
|
|
2850
|
+
names = fs__WEBPACK_IMPORTED_MODULE_2__.readdirSync(this.dir);
|
|
2851
|
+
}
|
|
2852
|
+
catch {
|
|
2853
|
+
return; // 폴더 없음 = 훅이 아직 한 번도 안 돌았다
|
|
2854
|
+
}
|
|
2855
|
+
for (const name of names) {
|
|
2856
|
+
if (!name.endsWith('.json')) {
|
|
2857
|
+
continue;
|
|
2858
|
+
}
|
|
2859
|
+
const full = path__WEBPACK_IMPORTED_MODULE_5__.join(this.dir, name);
|
|
2860
|
+
let data;
|
|
2861
|
+
try {
|
|
2862
|
+
data = JSON.parse(fs__WEBPACK_IMPORTED_MODULE_2__.readFileSync(full, 'utf8'));
|
|
2863
|
+
}
|
|
2864
|
+
catch {
|
|
2865
|
+
continue; // 훅이 쓰는 중 — 다음 폴링에서 다시 읽는다
|
|
2866
|
+
}
|
|
2867
|
+
// 지난 Tabby 실행 때 남은 파일 — 이걸 읽으면 죽은 세션들이 전부 "지금 활성 탭" 에
|
|
2868
|
+
// 묶여 버려서(resolveTab 규칙) 실제 세션이 엉뚱한 탭에 붙는다. 실제로 완료된 세션의
|
|
2869
|
+
// done 이 작업 중인 탭에 찍히는 증상이 이것이었다 (2026-08-28 실측). 읽지 말고 지운다
|
|
2870
|
+
if (data.ts < this.startedAt && !this.sessionTab.has(data.sessionId)) {
|
|
2871
|
+
try {
|
|
2872
|
+
fs__WEBPACK_IMPORTED_MODULE_2__.unlinkSync(full);
|
|
2873
|
+
}
|
|
2874
|
+
catch {
|
|
2875
|
+
// 지우기 실패해도 아래 apply 를 타지 않으므로 해는 없다
|
|
2876
|
+
}
|
|
2877
|
+
continue;
|
|
2878
|
+
}
|
|
2879
|
+
this.apply(data);
|
|
2880
|
+
}
|
|
2881
|
+
}
|
|
2882
|
+
/**
|
|
2883
|
+
* 보고 한 건을 반영한다. `status` / `label` 은 **각각 선택**이고, 빠진 쪽은 지금 값을 유지한다.
|
|
2884
|
+
*
|
|
2885
|
+
* 병합 규칙을 여기(플러그인)에 둔 이유 — 예전에는 훅 스크립트가 status 파일을 읽어
|
|
2886
|
+
* 직전 값과 합친 뒤 다시 쓰는 read-modify-write 를 했다. 그러면 진짜 상태가 파일과
|
|
2887
|
+
* 플러그인 메모리 두 곳에 살고 병합 규칙이 PowerShell 안에 숨는다. 라벨만 바꾸려 해도
|
|
2888
|
+
* 직전 status 를 채워 넣어야 했고(안 채우면 여기서 통째로 버려졌다), 파일이 밀리면
|
|
2889
|
+
* 표시가 어긋났다. 보내는 쪽은 "자기가 아는 것만" 보내고 합치는 건 상태를 실제로
|
|
2890
|
+
* 들고 있는 이쪽이 한다 — 그래야 어느 경로로 들어오든 결과가 같다.
|
|
2891
|
+
*/
|
|
2892
|
+
apply(data) {
|
|
2893
|
+
if (!data || !data.sessionId) {
|
|
2894
|
+
return;
|
|
2895
|
+
}
|
|
2896
|
+
const hasStatus = !!data.status;
|
|
2897
|
+
const hasLabel = typeof data.label === 'string' && !!data.label.trim();
|
|
2898
|
+
if (!hasStatus && !hasLabel) {
|
|
2899
|
+
return;
|
|
2900
|
+
}
|
|
2901
|
+
if (this.applied.get(data.sessionId) === data.ts) {
|
|
2902
|
+
return;
|
|
2903
|
+
}
|
|
2904
|
+
const tab = this.resolveTab(data.sessionId);
|
|
2905
|
+
if (!tab) {
|
|
2906
|
+
return;
|
|
2907
|
+
}
|
|
2908
|
+
this.applied.set(data.sessionId, data.ts);
|
|
2909
|
+
const label = (data.label || '').trim();
|
|
2910
|
+
if (hasStatus) {
|
|
2911
|
+
// 라벨이 안 왔으면 undefined 를 넘겨 기존 라벨을 지키게 한다 (setManual 규약)
|
|
2912
|
+
this.status.setManual(tab, data.status, hasLabel ? label : undefined);
|
|
2913
|
+
}
|
|
2914
|
+
else {
|
|
2915
|
+
// 라벨만 온 보고 — 상태도 고정 여부도 건드리지 않는다
|
|
2916
|
+
this.status.setLabel(tab, label);
|
|
2917
|
+
}
|
|
2918
|
+
// 탭 제목(customTitle)은 건드리지 않는다.
|
|
2919
|
+
// 제목을 라벨 원천으로 쓸 수 있는지 보려고 한동안 비워 뒀고, 판정은 났다 —
|
|
2920
|
+
// Claude Code 는 세션 첫 작업을 요약해 제목에 한 번 박고 그 뒤로 갱신하지 않는다.
|
|
2921
|
+
// 그래서 라벨은 Enter 시점 입력창에서 직접 읽고(claimEnterLabel), 사이드바는
|
|
2922
|
+
// 제목 대신 그 라벨을 제목 줄에 그린다(labelAsTitle). 여기서 제목을 덮어쓸 이유가 없다.
|
|
2923
|
+
}
|
|
2924
|
+
/**
|
|
2925
|
+
* 세션에 묶인 탭을 찾는다. 처음 보는 세션이면 지금 활성 탭에 묶는다.
|
|
2926
|
+
*
|
|
2927
|
+
* 단 그 탭을 이미 다른 세션이 점유했으면 **묶지 않는다**. 예전엔 이 검사가 없어서
|
|
2928
|
+
* 여러 세션이 한 탭에 겹쳐 붙었고, 서로의 상태를 덮어써 "작업 중인 탭이 완료로,
|
|
2929
|
+
* 끝난 탭이 진행 중으로" 뒤바뀌어 보였다. 못 묶은 세션은 사용자가 그 탭을 활성화하는
|
|
2930
|
+
* 순간(= 다음 보고 때) 제 짝을 찾는다.
|
|
2931
|
+
*/
|
|
2932
|
+
resolveTab(sessionId) {
|
|
2933
|
+
const known = this.sessionTab.get(sessionId);
|
|
2934
|
+
if (known && this.app.tabs.includes(known)) {
|
|
2935
|
+
return known;
|
|
2936
|
+
}
|
|
2937
|
+
const active = this.app.activeTab;
|
|
2938
|
+
if (!active) {
|
|
2939
|
+
return null;
|
|
2940
|
+
}
|
|
2941
|
+
const owner = this.tabSession.get(active);
|
|
2942
|
+
if (owner && owner !== sessionId) {
|
|
2943
|
+
return null;
|
|
2944
|
+
}
|
|
2945
|
+
this.sessionTab.set(sessionId, active);
|
|
2946
|
+
this.tabSession.set(active, sessionId);
|
|
2947
|
+
return active;
|
|
2948
|
+
}
|
|
2949
|
+
};
|
|
2950
|
+
WorkNotifyService = __decorate([
|
|
2951
|
+
(0,_angular_core__WEBPACK_IMPORTED_MODULE_0__.Injectable)({ providedIn: 'root' }),
|
|
2952
|
+
__metadata("design:paramtypes", [tabby_core__WEBPACK_IMPORTED_MODULE_1__.AppService,
|
|
2953
|
+
tabby_core__WEBPACK_IMPORTED_MODULE_1__.ConfigService,
|
|
2954
|
+
_status_service__WEBPACK_IMPORTED_MODULE_6__.WorkStatusService])
|
|
2955
|
+
], WorkNotifyService);
|
|
2956
|
+
|
|
2957
|
+
|
|
2958
|
+
|
|
2959
|
+
/***/ },
|
|
2960
|
+
|
|
2961
|
+
/***/ "./src/profile.service.ts"
|
|
2962
|
+
/*!********************************!*\
|
|
2963
|
+
!*** ./src/profile.service.ts ***!
|
|
2964
|
+
\********************************/
|
|
2965
|
+
(__unused_webpack_module, __webpack_exports__, __webpack_require__) {
|
|
2966
|
+
|
|
2967
|
+
__webpack_require__.r(__webpack_exports__);
|
|
2968
|
+
/* harmony export */ __webpack_require__.d(__webpack_exports__, {
|
|
2969
|
+
/* harmony export */ AgentDeckProfileService: () => (/* binding */ AgentDeckProfileService),
|
|
2970
|
+
/* harmony export */ ROOT_PROFILE_ID: () => (/* binding */ ROOT_PROFILE_ID)
|
|
2971
|
+
/* harmony export */ });
|
|
2972
|
+
/* harmony import */ var _angular_core__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! @angular/core */ "@angular/core");
|
|
2973
|
+
/* harmony import */ var _angular_core__WEBPACK_IMPORTED_MODULE_0___default = /*#__PURE__*/__webpack_require__.n(_angular_core__WEBPACK_IMPORTED_MODULE_0__);
|
|
2974
|
+
/* harmony import */ var tabby_core__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(/*! tabby-core */ "tabby-core");
|
|
2975
|
+
/* harmony import */ var tabby_core__WEBPACK_IMPORTED_MODULE_1___default = /*#__PURE__*/__webpack_require__.n(tabby_core__WEBPACK_IMPORTED_MODULE_1__);
|
|
2976
|
+
var __decorate = (undefined && undefined.__decorate) || function (decorators, target, key, desc) {
|
|
2977
|
+
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
2978
|
+
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
|
2979
|
+
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;
|
|
2980
|
+
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
2981
|
+
};
|
|
2982
|
+
var __metadata = (undefined && undefined.__metadata) || function (k, v) {
|
|
2983
|
+
if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
|
|
2984
|
+
};
|
|
2985
|
+
|
|
2986
|
+
|
|
2987
|
+
const ROOT_PROFILE_ID = 'agentdeck:root';
|
|
2988
|
+
/**
|
|
2989
|
+
* 새 탭이 항상 작업 루트에서 열리게 한다.
|
|
2990
|
+
*
|
|
2991
|
+
* Tabby 프로필을 하나 만들어 config 에 심고, 사용자가 아직 기본 프로필을
|
|
2992
|
+
* 직접 고른 적이 없으면 이것을 기본으로 지정한다.
|
|
2993
|
+
* 한 번 심은 뒤에는 사용자가 설정에서 바꾼 값을 존중한다 (매 기동 덮어쓰지 않음).
|
|
2994
|
+
*/
|
|
2995
|
+
let AgentDeckProfileService = class AgentDeckProfileService {
|
|
2996
|
+
constructor(app, config) {
|
|
2997
|
+
this.app = app;
|
|
2998
|
+
this.config = config;
|
|
2999
|
+
}
|
|
3000
|
+
init() {
|
|
3001
|
+
this.app.ready$.subscribe(() => this.ensureProfile());
|
|
3002
|
+
}
|
|
3003
|
+
ensureProfile() {
|
|
3004
|
+
const cfg = this.config.store.agentDeck;
|
|
3005
|
+
// 경로를 안 정했으면 만들지 않는다 — 남의 홈 디렉토리에 엉뚱한 프로필을 심지 않기 위해
|
|
3006
|
+
if (!cfg.rootProfile || !cfg.rootProfileCwd) {
|
|
3007
|
+
return;
|
|
3008
|
+
}
|
|
3009
|
+
const profiles = this.config.store.profiles || [];
|
|
3010
|
+
const existing = profiles.find((p) => p.id === ROOT_PROFILE_ID);
|
|
3011
|
+
const profile = {
|
|
3012
|
+
id: ROOT_PROFILE_ID,
|
|
3013
|
+
type: 'local',
|
|
3014
|
+
name: cfg.rootProfileName || 'Agent Root',
|
|
3015
|
+
options: {
|
|
3016
|
+
command: cfg.rootProfileCommand || 'powershell.exe',
|
|
3017
|
+
args: [...(cfg.rootProfileArgs || [])],
|
|
3018
|
+
cwd: cfg.rootProfileCwd,
|
|
3019
|
+
env: { ...(cfg.rootProfileEnv || {}) },
|
|
3020
|
+
// Tabby 를 이미 관리자로 띄웠다면 자식 프로세스가 권한을 물려받으므로
|
|
3021
|
+
// 이 값은 꺼두는 편이 낫다 (켜면 탭마다 UAC 창이 뜬다)
|
|
3022
|
+
runAsAdministrator: !!cfg.rootProfileAdmin,
|
|
3023
|
+
},
|
|
3024
|
+
};
|
|
3025
|
+
if (existing) {
|
|
3026
|
+
// 사용자가 설정 UI 에서 손댔을 수 있으므로 비어 있는 값만 보정한다
|
|
3027
|
+
if (!existing.options) {
|
|
3028
|
+
existing.options = profile.options;
|
|
3029
|
+
this.config.save();
|
|
3030
|
+
}
|
|
3031
|
+
else {
|
|
3032
|
+
let dirty = false;
|
|
3033
|
+
if (!existing.options.cwd) {
|
|
3034
|
+
existing.options.cwd = profile.options.cwd;
|
|
3035
|
+
dirty = true;
|
|
3036
|
+
}
|
|
3037
|
+
// npm 전역 .ps1 래퍼(claude 등)가 기본 실행 정책에 막히지 않도록 인자를 보정한다.
|
|
3038
|
+
// 사용자가 직접 인자를 넣어 뒀다면 건드리지 않는다.
|
|
3039
|
+
if (!existing.options.args || existing.options.args.length === 0) {
|
|
3040
|
+
existing.options.args = [...profile.options.args];
|
|
3041
|
+
dirty = true;
|
|
3042
|
+
}
|
|
3043
|
+
// 색 지원 환경변수는 사용자가 지정한 값을 덮지 않고 빠진 것만 채운다
|
|
3044
|
+
existing.options.env = existing.options.env || {};
|
|
3045
|
+
for (const [k, v] of Object.entries(profile.options.env)) {
|
|
3046
|
+
if (existing.options.env[k] === undefined) {
|
|
3047
|
+
existing.options.env[k] = v;
|
|
3048
|
+
dirty = true;
|
|
3049
|
+
}
|
|
3050
|
+
}
|
|
3051
|
+
if (dirty) {
|
|
3052
|
+
this.config.save();
|
|
3053
|
+
}
|
|
3054
|
+
}
|
|
3055
|
+
}
|
|
3056
|
+
else {
|
|
3057
|
+
profiles.push(profile);
|
|
3058
|
+
this.config.store.profiles = profiles;
|
|
3059
|
+
// 최초 1회만 기본 프로필로 지정
|
|
3060
|
+
if (!cfg.rootProfileClaimed) {
|
|
3061
|
+
this.config.store.terminal.profile = ROOT_PROFILE_ID;
|
|
3062
|
+
cfg.rootProfileClaimed = true;
|
|
3063
|
+
}
|
|
3064
|
+
this.config.save();
|
|
3065
|
+
}
|
|
3066
|
+
}
|
|
3067
|
+
};
|
|
3068
|
+
AgentDeckProfileService = __decorate([
|
|
3069
|
+
(0,_angular_core__WEBPACK_IMPORTED_MODULE_0__.Injectable)({ providedIn: 'root' }),
|
|
3070
|
+
__metadata("design:paramtypes", [tabby_core__WEBPACK_IMPORTED_MODULE_1__.AppService,
|
|
3071
|
+
tabby_core__WEBPACK_IMPORTED_MODULE_1__.ConfigService])
|
|
3072
|
+
], AgentDeckProfileService);
|
|
3073
|
+
|
|
3074
|
+
|
|
3075
|
+
|
|
3076
|
+
/***/ },
|
|
3077
|
+
|
|
3078
|
+
/***/ "./src/prompt.ts"
|
|
3079
|
+
/*!***********************!*\
|
|
3080
|
+
!*** ./src/prompt.ts ***!
|
|
3081
|
+
\***********************/
|
|
3082
|
+
(__unused_webpack_module, __webpack_exports__, __webpack_require__) {
|
|
3083
|
+
|
|
3084
|
+
__webpack_require__.r(__webpack_exports__);
|
|
3085
|
+
/* harmony export */ __webpack_require__.d(__webpack_exports__, {
|
|
3086
|
+
/* harmony export */ MAX_LABEL_LEN: () => (/* binding */ MAX_LABEL_LEN),
|
|
3087
|
+
/* harmony export */ extractPrompt: () => (/* binding */ extractPrompt)
|
|
3088
|
+
/* harmony export */ });
|
|
3089
|
+
/**
|
|
3090
|
+
* 터미널 화면에 그려진 입력창에서 지금 쓰여 있는 프롬프트를 뽑아낸다.
|
|
3091
|
+
*
|
|
3092
|
+
* 타이핑을 따로 세지 않고 화면을 읽는 이유 — 붙여넣기·↑ 히스토리·자동완성·IME 로 들어온 글자는
|
|
3093
|
+
* 키 이벤트로 안 오거나 원문과 다르다. 화면에 실제로 그려진 것이 곧 보낼 내용이다.
|
|
3094
|
+
*
|
|
3095
|
+
* 입력창 모양은 앱·버전마다 다르다. 옛 Claude Code / Codex 는 테두리 상자였고
|
|
3096
|
+
* ╭────────────────────╮
|
|
3097
|
+
* │ > 프롬프트 내용 │
|
|
3098
|
+
* ╰────────────────────╯
|
|
3099
|
+
* Claude Code 2.1.x 는 상자를 버리고 위아래 가로선 사이에 한 줄만 그린다.
|
|
3100
|
+
* ────────────────────────
|
|
3101
|
+
* ❯ 프롬프트 내용
|
|
3102
|
+
* ────────────────────────
|
|
3103
|
+
* ? for shortcuts
|
|
3104
|
+
* 머리글자도 ASCII `>` 가 아니라 `❯`(U+276F) 다 — 2026-09-01 화면 캡처로 확인.
|
|
3105
|
+
* `>` 만 찾던 정규식이 이걸 못 잡아 라벨이 한 번도 안 바뀌었다.
|
|
3106
|
+
*
|
|
3107
|
+
* 머리를 못 찾으면 빈 문자열을 준다 — 일반 셸이나 y/n 확인 같은 데서 엉뚱한 라벨이 박히느니
|
|
3108
|
+
* 라벨을 그대로 두는 편이 낫다.
|
|
3109
|
+
*
|
|
3110
|
+
* 화면 버퍼를 직접 받지 않고 줄 배열만 받는 순수 함수로 둔다 (xterm 없이 검증할 수 있게).
|
|
3111
|
+
*/
|
|
3112
|
+
/** 입력창의 첫 줄 표식 — ASCII `>` 와 꺾쇠 계열(❯ › ➜ ❭) 을 모두 받는다 */
|
|
3113
|
+
const PROMPT_HEAD_RE = /^[>❯›❭➜]\s*/;
|
|
3114
|
+
/**
|
|
3115
|
+
* 같은 꺾쇠를 선택지 커서로도 쓴다 (`❯ 1. Yes`). 그건 프롬프트가 아니다.
|
|
3116
|
+
* 번호 목록뿐 아니라 Yes/No 같은 무번호 선택지도 배제한다.
|
|
3117
|
+
*/
|
|
3118
|
+
const CHOICE_RE = /^[>❯›❭➜]\s*(\d+[.)]\s|(?:Yes|No)\b)/i;
|
|
3119
|
+
/** 입력창 아래 테두리 — 상자 모서리(╰└) 또는 가로선만으로 된 줄 */
|
|
3120
|
+
const BOX_BOTTOM_RE = /^[\s│┃|]*[─━═-]{3,}[\s│┃|]*$|[╰└]/;
|
|
3121
|
+
/** 이보다 짧으면 라벨로 쓰지 않는다 (y/n 응답 등) */
|
|
3122
|
+
const MIN_PROMPT_LEN = 2;
|
|
3123
|
+
/** 사이드바에 들어가는 라벨 길이 */
|
|
3124
|
+
const MAX_LABEL_LEN = 40;
|
|
3125
|
+
/** 입력창 테두리(│ ┃ 등)와 좌우 여백을 떼어낸 알맹이 */
|
|
3126
|
+
function stripBox(line) {
|
|
3127
|
+
return line.replace(/^[\s│┃|╭╰┌└─╮╯┐┘]+/, '').replace(/[\s│┃|╮╯┐┘─]+$/, '').trim();
|
|
3128
|
+
}
|
|
3129
|
+
function extractPrompt(lines) {
|
|
3130
|
+
// 아래에서 위로 머리 줄을 찾는다 (마지막 입력창이 곧 지금 입력창)
|
|
3131
|
+
let head = -1;
|
|
3132
|
+
for (let i = lines.length - 1; i >= 0; i--) {
|
|
3133
|
+
const body = stripBox(lines[i]);
|
|
3134
|
+
if (!PROMPT_HEAD_RE.test(body)) {
|
|
3135
|
+
continue;
|
|
3136
|
+
}
|
|
3137
|
+
if (CHOICE_RE.test(body)) {
|
|
3138
|
+
return ''; // 선택지 커서 — 여기서 위로 더 올라가면 옛 프롬프트를 주워온다
|
|
3139
|
+
}
|
|
3140
|
+
head = i;
|
|
3141
|
+
break;
|
|
3142
|
+
}
|
|
3143
|
+
if (head < 0) {
|
|
3144
|
+
return '';
|
|
3145
|
+
}
|
|
3146
|
+
const parts = [stripBox(lines[head]).replace(PROMPT_HEAD_RE, '')];
|
|
3147
|
+
for (let i = head + 1; i < lines.length; i++) {
|
|
3148
|
+
if (BOX_BOTTOM_RE.test(lines[i])) {
|
|
3149
|
+
break; // 입력창 끝 — 그 아래는 힌트 줄이라 프롬프트가 아니다
|
|
3150
|
+
}
|
|
3151
|
+
const body = stripBox(lines[i]);
|
|
3152
|
+
if (body) {
|
|
3153
|
+
parts.push(body);
|
|
3154
|
+
}
|
|
3155
|
+
}
|
|
3156
|
+
const text = parts.join(' ').replace(/\s+/g, ' ').trim();
|
|
3157
|
+
if (text.length < MIN_PROMPT_LEN) {
|
|
3158
|
+
return ''; // y/n 같은 한 글자 응답 — 라벨로 쓸 것이 못 된다
|
|
3159
|
+
}
|
|
3160
|
+
return text.length > MAX_LABEL_LEN ? text.slice(0, MAX_LABEL_LEN).trimEnd() + '...' : text;
|
|
3161
|
+
}
|
|
3162
|
+
|
|
3163
|
+
|
|
3164
|
+
/***/ },
|
|
3165
|
+
|
|
3166
|
+
/***/ "./src/status.service.ts"
|
|
3167
|
+
/*!*******************************!*\
|
|
3168
|
+
!*** ./src/status.service.ts ***!
|
|
3169
|
+
\*******************************/
|
|
3170
|
+
(__unused_webpack_module, __webpack_exports__, __webpack_require__) {
|
|
3171
|
+
|
|
3172
|
+
__webpack_require__.r(__webpack_exports__);
|
|
3173
|
+
/* harmony export */ __webpack_require__.d(__webpack_exports__, {
|
|
3174
|
+
/* harmony export */ WorkStatusService: () => (/* binding */ WorkStatusService)
|
|
3175
|
+
/* harmony export */ });
|
|
3176
|
+
/* harmony import */ var _angular_core__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! @angular/core */ "@angular/core");
|
|
3177
|
+
/* harmony import */ var _angular_core__WEBPACK_IMPORTED_MODULE_0___default = /*#__PURE__*/__webpack_require__.n(_angular_core__WEBPACK_IMPORTED_MODULE_0__);
|
|
3178
|
+
/* harmony import */ var rxjs__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(/*! rxjs */ "rxjs");
|
|
3179
|
+
/* harmony import */ var rxjs__WEBPACK_IMPORTED_MODULE_1___default = /*#__PURE__*/__webpack_require__.n(rxjs__WEBPACK_IMPORTED_MODULE_1__);
|
|
3180
|
+
var __decorate = (undefined && undefined.__decorate) || function (decorators, target, key, desc) {
|
|
3181
|
+
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
3182
|
+
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
|
3183
|
+
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;
|
|
3184
|
+
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
3185
|
+
};
|
|
3186
|
+
|
|
3187
|
+
|
|
3188
|
+
/**
|
|
3189
|
+
* 탭별 작업 상태 보관소.
|
|
3190
|
+
* 상태의 출처는 3가지 — 자동 감지(decorator), OSC 시퀀스(Claude Code 훅), 사이드바에서의 수동 지정.
|
|
3191
|
+
* pinned 가 true 면 자동 감지가 값을 덮어쓰지 않는다.
|
|
3192
|
+
*/
|
|
3193
|
+
let WorkStatusService = class WorkStatusService {
|
|
3194
|
+
constructor() {
|
|
3195
|
+
this.states = new Map();
|
|
3196
|
+
this.changed = new rxjs__WEBPACK_IMPORTED_MODULE_1__.Subject();
|
|
3197
|
+
/** 진단용 카운터 — decorator 가 실제로 붙었는지, 출력이 흘러오는지 확인한다 */
|
|
3198
|
+
this.debug = {
|
|
3199
|
+
attached: 0,
|
|
3200
|
+
outputs: 0,
|
|
3201
|
+
oscHits: 0,
|
|
3202
|
+
titleHits: 0,
|
|
3203
|
+
};
|
|
3204
|
+
}
|
|
3205
|
+
get changed$() { return this.changed; }
|
|
3206
|
+
get(tab) {
|
|
3207
|
+
let s = this.states.get(tab);
|
|
3208
|
+
if (!s) {
|
|
3209
|
+
s = { status: 'idle', label: '', since: Date.now(), pinned: false, lastOutput: 0 };
|
|
3210
|
+
this.states.set(tab, s);
|
|
3211
|
+
}
|
|
3212
|
+
return s;
|
|
3213
|
+
}
|
|
3214
|
+
/** 자동 감지용 — pinned 상태는 건드리지 않는다 */
|
|
3215
|
+
setAuto(tab, status) {
|
|
3216
|
+
const s = this.get(tab);
|
|
3217
|
+
if (s.pinned || s.status === status) {
|
|
3218
|
+
return;
|
|
3219
|
+
}
|
|
3220
|
+
s.status = status;
|
|
3221
|
+
s.since = Date.now();
|
|
3222
|
+
this.changed.next();
|
|
3223
|
+
}
|
|
3224
|
+
/** 수동/훅 지정 — 이후 자동 감지보다 우선한다 */
|
|
3225
|
+
setManual(tab, status, label) {
|
|
3226
|
+
const s = this.get(tab);
|
|
3227
|
+
s.status = status;
|
|
3228
|
+
s.since = Date.now();
|
|
3229
|
+
s.pinned = true;
|
|
3230
|
+
if (label !== undefined) {
|
|
3231
|
+
s.label = label;
|
|
3232
|
+
}
|
|
3233
|
+
this.changed.next();
|
|
3234
|
+
}
|
|
3235
|
+
setLabel(tab, label) {
|
|
3236
|
+
this.get(tab).label = label;
|
|
3237
|
+
this.changed.next();
|
|
3238
|
+
}
|
|
3239
|
+
/** 수동 고정을 풀고 자동 감지로 되돌린다 */
|
|
3240
|
+
unpin(tab) {
|
|
3241
|
+
const s = this.get(tab);
|
|
3242
|
+
s.pinned = false;
|
|
3243
|
+
s.status = 'idle';
|
|
3244
|
+
s.since = Date.now();
|
|
3245
|
+
this.changed.next();
|
|
3246
|
+
}
|
|
3247
|
+
markOutput(tab) {
|
|
3248
|
+
this.get(tab).lastOutput = Date.now();
|
|
3249
|
+
}
|
|
3250
|
+
forget(tab) {
|
|
3251
|
+
this.states.delete(tab);
|
|
3252
|
+
this.changed.next();
|
|
3253
|
+
}
|
|
3254
|
+
/** 사이드바가 매 틱 호출 — idle 전환만 담당하고 화면 갱신 필요 여부를 돌려준다 */
|
|
3255
|
+
tickIdle(tab, idleAfterMs) {
|
|
3256
|
+
const s = this.get(tab);
|
|
3257
|
+
if (s.pinned || s.status !== 'running') {
|
|
3258
|
+
return false;
|
|
3259
|
+
}
|
|
3260
|
+
if (s.lastOutput && Date.now() - s.lastOutput > idleAfterMs) {
|
|
3261
|
+
s.status = 'idle';
|
|
3262
|
+
s.since = Date.now();
|
|
3263
|
+
return true;
|
|
3264
|
+
}
|
|
3265
|
+
return false;
|
|
3266
|
+
}
|
|
3267
|
+
};
|
|
3268
|
+
WorkStatusService = __decorate([
|
|
3269
|
+
(0,_angular_core__WEBPACK_IMPORTED_MODULE_0__.Injectable)({ providedIn: 'root' })
|
|
3270
|
+
], WorkStatusService);
|
|
3271
|
+
|
|
3272
|
+
|
|
3273
|
+
|
|
3274
|
+
/***/ },
|
|
3275
|
+
|
|
3276
|
+
/***/ "net"
|
|
3277
|
+
/*!**********************!*\
|
|
3278
|
+
!*** external "net" ***!
|
|
3279
|
+
\**********************/
|
|
3280
|
+
(module) {
|
|
3281
|
+
|
|
3282
|
+
module.exports = require("net");
|
|
3283
|
+
|
|
3284
|
+
/***/ },
|
|
3285
|
+
|
|
3286
|
+
/***/ "@angular/common"
|
|
3287
|
+
/*!**********************************!*\
|
|
3288
|
+
!*** external "@angular/common" ***!
|
|
3289
|
+
\**********************************/
|
|
3290
|
+
(module) {
|
|
3291
|
+
|
|
3292
|
+
module.exports = __WEBPACK_EXTERNAL_MODULE__angular_common__;
|
|
3293
|
+
|
|
3294
|
+
/***/ },
|
|
3295
|
+
|
|
3296
|
+
/***/ "@angular/core"
|
|
3297
|
+
/*!********************************!*\
|
|
3298
|
+
!*** external "@angular/core" ***!
|
|
3299
|
+
\********************************/
|
|
3300
|
+
(module) {
|
|
3301
|
+
|
|
3302
|
+
module.exports = __WEBPACK_EXTERNAL_MODULE__angular_core__;
|
|
3303
|
+
|
|
3304
|
+
/***/ },
|
|
3305
|
+
|
|
3306
|
+
/***/ "electron"
|
|
3307
|
+
/*!***************************!*\
|
|
3308
|
+
!*** external "electron" ***!
|
|
3309
|
+
\***************************/
|
|
3310
|
+
(module) {
|
|
3311
|
+
|
|
3312
|
+
if(typeof __WEBPACK_EXTERNAL_MODULE_electron__ === 'undefined') { const e = new Error("Cannot find module 'electron'"); e.code = 'MODULE_NOT_FOUND'; throw e; }
|
|
3313
|
+
|
|
3314
|
+
module.exports = __WEBPACK_EXTERNAL_MODULE_electron__;
|
|
3315
|
+
|
|
3316
|
+
/***/ },
|
|
3317
|
+
|
|
3318
|
+
/***/ "fs"
|
|
3319
|
+
/*!*********************!*\
|
|
3320
|
+
!*** external "fs" ***!
|
|
3321
|
+
\*********************/
|
|
3322
|
+
(module) {
|
|
3323
|
+
|
|
3324
|
+
module.exports = __WEBPACK_EXTERNAL_MODULE_fs__;
|
|
3325
|
+
|
|
3326
|
+
/***/ },
|
|
3327
|
+
|
|
3328
|
+
/***/ "os"
|
|
3329
|
+
/*!*********************!*\
|
|
3330
|
+
!*** external "os" ***!
|
|
3331
|
+
\*********************/
|
|
3332
|
+
(module) {
|
|
3333
|
+
|
|
3334
|
+
module.exports = __WEBPACK_EXTERNAL_MODULE_os__;
|
|
3335
|
+
|
|
3336
|
+
/***/ },
|
|
3337
|
+
|
|
3338
|
+
/***/ "path"
|
|
3339
|
+
/*!***********************!*\
|
|
3340
|
+
!*** external "path" ***!
|
|
3341
|
+
\***********************/
|
|
3342
|
+
(module) {
|
|
3343
|
+
|
|
3344
|
+
module.exports = __WEBPACK_EXTERNAL_MODULE_path__;
|
|
3345
|
+
|
|
3346
|
+
/***/ },
|
|
3347
|
+
|
|
3348
|
+
/***/ "rxjs"
|
|
3349
|
+
/*!***********************!*\
|
|
3350
|
+
!*** external "rxjs" ***!
|
|
3351
|
+
\***********************/
|
|
3352
|
+
(module) {
|
|
3353
|
+
|
|
3354
|
+
module.exports = __WEBPACK_EXTERNAL_MODULE_rxjs__;
|
|
3355
|
+
|
|
3356
|
+
/***/ },
|
|
3357
|
+
|
|
3358
|
+
/***/ "tabby-core"
|
|
3359
|
+
/*!*****************************!*\
|
|
3360
|
+
!*** external "tabby-core" ***!
|
|
3361
|
+
\*****************************/
|
|
3362
|
+
(module) {
|
|
3363
|
+
|
|
3364
|
+
module.exports = __WEBPACK_EXTERNAL_MODULE_tabby_core__;
|
|
3365
|
+
|
|
3366
|
+
/***/ },
|
|
3367
|
+
|
|
3368
|
+
/***/ "tabby-settings"
|
|
3369
|
+
/*!*********************************!*\
|
|
3370
|
+
!*** external "tabby-settings" ***!
|
|
3371
|
+
\*********************************/
|
|
3372
|
+
(module) {
|
|
3373
|
+
|
|
3374
|
+
module.exports = __WEBPACK_EXTERNAL_MODULE_tabby_settings__;
|
|
3375
|
+
|
|
3376
|
+
/***/ },
|
|
3377
|
+
|
|
3378
|
+
/***/ "tabby-terminal"
|
|
3379
|
+
/*!*********************************!*\
|
|
3380
|
+
!*** external "tabby-terminal" ***!
|
|
3381
|
+
\*********************************/
|
|
3382
|
+
(module) {
|
|
3383
|
+
|
|
3384
|
+
module.exports = __WEBPACK_EXTERNAL_MODULE_tabby_terminal__;
|
|
3385
|
+
|
|
3386
|
+
/***/ }
|
|
3387
|
+
|
|
3388
|
+
/******/ });
|
|
3389
|
+
/************************************************************************/
|
|
3390
|
+
/******/ // The module cache
|
|
3391
|
+
/******/ const __webpack_module_cache__ = {};
|
|
3392
|
+
/******/
|
|
3393
|
+
/******/ // The require function
|
|
3394
|
+
/******/ function __webpack_require__(moduleId) {
|
|
3395
|
+
/******/ // Check if module is in cache
|
|
3396
|
+
/******/ const cachedModule = __webpack_module_cache__[moduleId];
|
|
3397
|
+
/******/ if (cachedModule !== undefined) {
|
|
3398
|
+
/******/ return cachedModule.exports;
|
|
3399
|
+
/******/ }
|
|
3400
|
+
/******/ // Create a new module (and put it into the cache)
|
|
3401
|
+
/******/ const module = __webpack_module_cache__[moduleId] = {
|
|
3402
|
+
/******/ id: moduleId,
|
|
3403
|
+
/******/ // no module.loaded needed
|
|
3404
|
+
/******/ exports: {}
|
|
3405
|
+
/******/ };
|
|
3406
|
+
/******/
|
|
3407
|
+
/******/ // Execute the module function
|
|
3408
|
+
/******/ if (!(moduleId in __webpack_modules__)) {
|
|
3409
|
+
/******/ delete __webpack_module_cache__[moduleId];
|
|
3410
|
+
/******/ const e = new Error("Cannot find module '" + moduleId + "'");
|
|
3411
|
+
/******/ e.code = 'MODULE_NOT_FOUND';
|
|
3412
|
+
/******/ throw e;
|
|
3413
|
+
/******/ }
|
|
3414
|
+
/******/ __webpack_modules__[moduleId](module, module.exports, __webpack_require__);
|
|
3415
|
+
/******/
|
|
3416
|
+
/******/ // Return the exports of the module
|
|
3417
|
+
/******/ return module.exports;
|
|
3418
|
+
/******/ }
|
|
3419
|
+
/******/
|
|
3420
|
+
/************************************************************************/
|
|
3421
|
+
/******/ /* webpack/runtime/compat get default export */
|
|
3422
|
+
/******/ // getDefaultExport function for compatibility with non-harmony modules
|
|
3423
|
+
/******/ __webpack_require__.n = (module) => {
|
|
3424
|
+
/******/ const getter = module && module.__esModule ?
|
|
3425
|
+
/******/ () => (module['default']) :
|
|
3426
|
+
/******/ () => (module);
|
|
3427
|
+
/******/ __webpack_require__.d(getter, { a: getter });
|
|
3428
|
+
/******/ return getter;
|
|
3429
|
+
/******/ };
|
|
3430
|
+
/******/
|
|
3431
|
+
/******/ /* webpack/runtime/define property getters */
|
|
3432
|
+
/******/ // define getter/value functions for harmony exports
|
|
3433
|
+
/******/ __webpack_require__.d = (exports, definition) => {
|
|
3434
|
+
/******/ for(var key in definition) {
|
|
3435
|
+
/******/ if(__webpack_require__.o(definition, key) && !__webpack_require__.o(exports, key)) {
|
|
3436
|
+
/******/ Object.defineProperty(exports, key, { enumerable: true, get: definition[key] });
|
|
3437
|
+
/******/ }
|
|
3438
|
+
/******/ }
|
|
3439
|
+
/******/ };
|
|
3440
|
+
/******/
|
|
3441
|
+
/******/ /* webpack/runtime/hasOwnProperty shorthand */
|
|
3442
|
+
/******/ __webpack_require__.o = (obj, prop) => (Object.prototype.hasOwnProperty.call(obj, prop));
|
|
3443
|
+
/******/
|
|
3444
|
+
/******/ /* webpack/runtime/make namespace object */
|
|
3445
|
+
/******/ // define __esModule on exports
|
|
3446
|
+
/******/ __webpack_require__.r = (exports) => {
|
|
3447
|
+
/******/ Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
|
|
3448
|
+
/******/ Object.defineProperty(exports, '__esModule', { value: true });
|
|
3449
|
+
/******/ };
|
|
3450
|
+
/******/
|
|
3451
|
+
/******/ /* webpack/runtime/nonce */
|
|
3452
|
+
/******/ __webpack_require__.nc = undefined;
|
|
3453
|
+
/******/
|
|
3454
|
+
/************************************************************************/
|
|
3455
|
+
let __webpack_exports__ = {};
|
|
3456
|
+
// This entry needs to be wrapped in an IIFE because it needs to be isolated against other modules in the chunk.
|
|
3457
|
+
(() => {
|
|
3458
|
+
/*!**********************!*\
|
|
3459
|
+
!*** ./src/index.ts ***!
|
|
3460
|
+
\**********************/
|
|
3461
|
+
__webpack_require__.r(__webpack_exports__);
|
|
3462
|
+
/* harmony export */ __webpack_require__.d(__webpack_exports__, {
|
|
3463
|
+
/* harmony export */ STATUS_STYLES: () => (/* reexport safe */ _api__WEBPACK_IMPORTED_MODULE_11__.STATUS_STYLES),
|
|
3464
|
+
/* harmony export */ "default": () => (__WEBPACK_DEFAULT_EXPORT__)
|
|
3465
|
+
/* harmony export */ });
|
|
3466
|
+
/* harmony import */ var _angular_core__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! @angular/core */ "@angular/core");
|
|
3467
|
+
/* harmony import */ var _angular_core__WEBPACK_IMPORTED_MODULE_0___default = /*#__PURE__*/__webpack_require__.n(_angular_core__WEBPACK_IMPORTED_MODULE_0__);
|
|
3468
|
+
/* harmony import */ var _angular_common__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(/*! @angular/common */ "@angular/common");
|
|
3469
|
+
/* harmony import */ var _angular_common__WEBPACK_IMPORTED_MODULE_1___default = /*#__PURE__*/__webpack_require__.n(_angular_common__WEBPACK_IMPORTED_MODULE_1__);
|
|
3470
|
+
/* harmony import */ var tabby_core__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(/*! tabby-core */ "tabby-core");
|
|
3471
|
+
/* harmony import */ var tabby_core__WEBPACK_IMPORTED_MODULE_2___default = /*#__PURE__*/__webpack_require__.n(tabby_core__WEBPACK_IMPORTED_MODULE_2__);
|
|
3472
|
+
/* harmony import */ var tabby_terminal__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(/*! tabby-terminal */ "tabby-terminal");
|
|
3473
|
+
/* harmony import */ var tabby_terminal__WEBPACK_IMPORTED_MODULE_3___default = /*#__PURE__*/__webpack_require__.n(tabby_terminal__WEBPACK_IMPORTED_MODULE_3__);
|
|
3474
|
+
/* harmony import */ var _config__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(/*! ./config */ "./src/config.ts");
|
|
3475
|
+
/* harmony import */ var _hotkeys__WEBPACK_IMPORTED_MODULE_5__ = __webpack_require__(/*! ./hotkeys */ "./src/hotkeys.ts");
|
|
3476
|
+
/* harmony import */ var _decorator__WEBPACK_IMPORTED_MODULE_6__ = __webpack_require__(/*! ./decorator */ "./src/decorator.ts");
|
|
3477
|
+
/* harmony import */ var _deck_service__WEBPACK_IMPORTED_MODULE_7__ = __webpack_require__(/*! ./deck.service */ "./src/deck.service.ts");
|
|
3478
|
+
/* harmony import */ var _profile_service__WEBPACK_IMPORTED_MODULE_8__ = __webpack_require__(/*! ./profile.service */ "./src/profile.service.ts");
|
|
3479
|
+
/* harmony import */ var _notify_service__WEBPACK_IMPORTED_MODULE_9__ = __webpack_require__(/*! ./notify.service */ "./src/notify.service.ts");
|
|
3480
|
+
/* harmony import */ var _styles_scss__WEBPACK_IMPORTED_MODULE_10__ = __webpack_require__(/*! ./styles.scss */ "./src/styles.scss");
|
|
3481
|
+
/* harmony import */ var _api__WEBPACK_IMPORTED_MODULE_11__ = __webpack_require__(/*! ./api */ "./src/api.ts");
|
|
3482
|
+
var __decorate = (undefined && undefined.__decorate) || function (decorators, target, key, desc) {
|
|
3483
|
+
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
3484
|
+
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
|
3485
|
+
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;
|
|
3486
|
+
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
3487
|
+
};
|
|
3488
|
+
var __metadata = (undefined && undefined.__metadata) || function (k, v) {
|
|
3489
|
+
if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
|
|
3490
|
+
};
|
|
3491
|
+
|
|
3492
|
+
|
|
3493
|
+
|
|
3494
|
+
|
|
3495
|
+
|
|
3496
|
+
|
|
3497
|
+
|
|
3498
|
+
|
|
3499
|
+
|
|
3500
|
+
|
|
3501
|
+
|
|
3502
|
+
|
|
3503
|
+
let AgentDeckModule = class AgentDeckModule {
|
|
3504
|
+
constructor(deck, profiles, notify) {
|
|
3505
|
+
// 세 서비스 모두 app.ready$ 를 기다렸다가 스스로 붙는다
|
|
3506
|
+
deck.init();
|
|
3507
|
+
profiles.init();
|
|
3508
|
+
notify.init();
|
|
3509
|
+
}
|
|
3510
|
+
};
|
|
3511
|
+
AgentDeckModule = __decorate([
|
|
3512
|
+
(0,_angular_core__WEBPACK_IMPORTED_MODULE_0__.NgModule)({
|
|
3513
|
+
imports: [
|
|
3514
|
+
_angular_common__WEBPACK_IMPORTED_MODULE_1__.CommonModule,
|
|
3515
|
+
(tabby_core__WEBPACK_IMPORTED_MODULE_2___default()),
|
|
3516
|
+
],
|
|
3517
|
+
providers: [
|
|
3518
|
+
{ provide: tabby_core__WEBPACK_IMPORTED_MODULE_2__.ConfigProvider, useClass: _config__WEBPACK_IMPORTED_MODULE_4__.AgentDeckConfigProvider, multi: true },
|
|
3519
|
+
{ provide: tabby_core__WEBPACK_IMPORTED_MODULE_2__.HotkeyProvider, useClass: _hotkeys__WEBPACK_IMPORTED_MODULE_5__.AgentDeckHotkeyProvider, multi: true },
|
|
3520
|
+
{ provide: tabby_terminal__WEBPACK_IMPORTED_MODULE_3__.TerminalDecorator, useClass: _decorator__WEBPACK_IMPORTED_MODULE_6__.AgentDeckDecorator, multi: true },
|
|
3521
|
+
],
|
|
3522
|
+
}),
|
|
3523
|
+
__metadata("design:paramtypes", [_deck_service__WEBPACK_IMPORTED_MODULE_7__.AgentDeckService,
|
|
3524
|
+
_profile_service__WEBPACK_IMPORTED_MODULE_8__.AgentDeckProfileService,
|
|
3525
|
+
_notify_service__WEBPACK_IMPORTED_MODULE_9__.WorkNotifyService])
|
|
3526
|
+
], AgentDeckModule);
|
|
3527
|
+
/* harmony default export */ const __WEBPACK_DEFAULT_EXPORT__ = (AgentDeckModule);
|
|
3528
|
+
|
|
3529
|
+
})();
|
|
3530
|
+
|
|
3531
|
+
/******/ return __webpack_exports__;
|
|
3532
|
+
/******/ })()
|
|
3533
|
+
;
|
|
3534
|
+
});
|
|
3535
|
+
//# sourceMappingURL=index.js.map
|