tabby-agentdeck 0.2.0 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -124,10 +124,22 @@ ___CSS_LOADER_EXPORT___.push([module.id, `body.agentdeck-active.ad-bg-clip .cont
124
124
  }
125
125
  #agentdeck-sidebar .ad-head-count {
126
126
  -webkit-app-region: no-drag;
127
+ display: flex;
128
+ align-items: center;
129
+ gap: 6px;
130
+ flex: 0 0 auto;
131
+ white-space: nowrap;
132
+ letter-spacing: 0;
133
+ color: rgba(255, 255, 255, 0.55);
134
+ }
135
+ #agentdeck-sidebar .ad-cnt {
127
136
  padding: 1px 6px;
128
137
  border-radius: 8px;
129
138
  background: rgba(255, 255, 255, 0.08);
130
- color: rgba(255, 255, 255, 0.55);
139
+ color: var(--ad-color, rgba(255, 255, 255, 0.55));
140
+ font-size: 10px;
141
+ line-height: 1.3;
142
+ font-variant-numeric: tabular-nums;
131
143
  }
132
144
  #agentdeck-sidebar .ad-list {
133
145
  flex: 1 1 auto;
@@ -195,7 +207,12 @@ ___CSS_LOADER_EXPORT___.push([module.id, `body.agentdeck-active.ad-bg-clip .cont
195
207
  color: rgba(255, 255, 255, 0.45);
196
208
  }
197
209
  #agentdeck-sidebar .ad-badge {
198
- flex: 0 0 auto;
210
+ flex: 0 1 auto;
211
+ min-width: 0;
212
+ max-width: 100%;
213
+ overflow: hidden;
214
+ text-overflow: ellipsis;
215
+ white-space: nowrap;
199
216
  color: var(--ad-color, #6b727d);
200
217
  font-weight: 600;
201
218
  }
@@ -378,7 +395,7 @@ body.agentdeck-active .window {
378
395
  flex-direction: row;
379
396
  position: relative;
380
397
  background: var(--ad-panel-bg, var(--bs-body-bg, #1e2127));
381
- }`, "",{"version":3,"sources":["webpack://./src/styles.scss"],"names":[],"mappings":"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","sourcesContent":["// AgentDeck 사이드바 스타일\r\n// Tabby 테마 변수(--bs-*)를 우선 쓰되, 없을 때를 대비해 폴백 색을 함께 준다.\r\n\r\n// tabby-background 의 배경 이미지 레이어(.content-tab-active::before)를 터미널 영역으로 자른다.\r\n// 좌표 변수(--ad-bg-*)는 deck.service 의 syncBackgroundClip 이 relayout 마다 내려준다.\r\n// 그 플러그인은 런타임에 <style> 을 만들어 head 뒤쪽에 붙이므로 순서로는 못 이긴다 — !important 필수.\r\nbody.agentdeck-active.ad-bg-clip .content-tab-active::before {\r\n left: var(--ad-bg-left) !important;\r\n top: var(--ad-bg-top) !important;\r\n right: auto !important;\r\n bottom: auto !important;\r\n width: var(--ad-bg-width) !important;\r\n height: var(--ad-bg-height) !important;\r\n}\r\n\r\n#agentdeck-sidebar {\r\n display: flex;\r\n flex-direction: column;\r\n // 실제 위치와 크기는 deck.service 의 relayout 이 인라인으로 준다 (도킹 방향에 따라 달라짐)\r\n z-index: 5;\r\n overflow: hidden;\r\n // --ad-panel-bg 는 agentdeck 이 sidebarOpacity 로 내려준다 (배경 이미지가 비치게)\r\n background: var(--ad-panel-bg, var(--bs-body-bg, #1e2127));\r\n font-size: 12px;\r\n user-select: none;\r\n -webkit-app-region: no-drag;\r\n\r\n // 붙어 있는 쪽 반대편에만 경계선을 그린다\r\n &.ad-dock-right { border-left: 1px solid rgba(255, 255, 255, 0.08); }\r\n &.ad-dock-left { border-right: 1px solid rgba(255, 255, 255, 0.08); }\r\n &.ad-dock-top { border-bottom: 1px solid rgba(255, 255, 255, 0.08); }\r\n &.ad-dock-bottom { border-top: 1px solid rgba(255, 255, 255, 0.08); }\r\n\r\n // 상/하 도킹은 세로가 짧으므로 목록을 가로로 흘린다\r\n &.ad-dock-top,\r\n &.ad-dock-bottom {\r\n .ad-list {\r\n display: flex;\r\n flex-wrap: wrap;\r\n align-content: flex-start;\r\n gap: 0 6px;\r\n overflow-y: auto;\r\n }\r\n\r\n .ad-tab {\r\n flex: 0 0 220px;\r\n }\r\n }\r\n\r\n .ad-head {\r\n display: flex;\r\n align-items: center;\r\n justify-content: space-between;\r\n padding: 10px 12px 8px;\r\n letter-spacing: 0.12em;\r\n font-size: 10px;\r\n font-weight: 700;\r\n color: rgba(255, 255, 255, 0.4);\r\n // 헤더를 잡아 끌면 도킹 방향을 바꾼다 (OS 창 이동에 뺏기면 안 되므로 no-drag)\r\n -webkit-app-region: no-drag;\r\n cursor: grab;\r\n\r\n &:active {\r\n cursor: grabbing;\r\n }\r\n }\r\n\r\n // 경계선 — 잡아 끌면 크기가 바뀐다. 붙어 있는 쪽 반대편 가장자리에 놓는다\r\n .ad-resize {\r\n position: absolute;\r\n z-index: 6;\r\n background: transparent;\r\n transition: background 0.12s ease;\r\n\r\n &:hover,\r\n body.ad-resizing & {\r\n background: rgba(97, 175, 239, 0.5);\r\n }\r\n }\r\n\r\n .ad-resize-right,\r\n .ad-resize-left {\r\n top: 0;\r\n bottom: 0;\r\n width: 6px;\r\n cursor: col-resize;\r\n }\r\n\r\n .ad-resize-right { left: 0; }\r\n .ad-resize-left { right: 0; }\r\n\r\n .ad-resize-top,\r\n .ad-resize-bottom {\r\n left: 0;\r\n right: 0;\r\n height: 6px;\r\n cursor: row-resize;\r\n }\r\n\r\n .ad-resize-bottom { top: 0; }\r\n .ad-resize-top { bottom: 0; }\r\n\r\n .ad-head-count {\r\n -webkit-app-region: no-drag;\r\n padding: 1px 6px;\r\n border-radius: 8px;\r\n background: rgba(255, 255, 255, 0.08);\r\n color: rgba(255, 255, 255, 0.55);\r\n }\r\n\r\n // 요구 2-1: 탭이 많아지면 이 영역만 스크롤된다\r\n .ad-list {\r\n flex: 1 1 auto;\r\n overflow-y: auto;\r\n overflow-x: hidden;\r\n padding: 2px 6px 8px;\r\n\r\n &::-webkit-scrollbar {\r\n width: 8px;\r\n }\r\n\r\n &::-webkit-scrollbar-thumb {\r\n background: rgba(255, 255, 255, 0.14);\r\n border-radius: 4px;\r\n\r\n &:hover {\r\n background: rgba(255, 255, 255, 0.24);\r\n }\r\n }\r\n }\r\n\r\n .ad-tab {\r\n position: relative;\r\n display: flex;\r\n align-items: stretch;\r\n gap: 8px;\r\n margin-bottom: 3px;\r\n padding: 7px 6px 7px 0;\r\n border-radius: 5px;\r\n cursor: pointer;\r\n transition: background 0.1s ease;\r\n\r\n &:hover {\r\n background: rgba(255, 255, 255, 0.05);\r\n\r\n .ad-close {\r\n opacity: 0.65;\r\n }\r\n }\r\n\r\n &.active {\r\n background: rgba(255, 255, 255, 0.1);\r\n\r\n .ad-title {\r\n color: #fff;\r\n }\r\n }\r\n }\r\n\r\n // 상태색 표시 바\r\n .ad-bar {\r\n flex: 0 0 3px;\r\n border-radius: 2px;\r\n background: var(--ad-color, #6b727d);\r\n }\r\n\r\n .ad-body {\r\n flex: 1 1 auto;\r\n min-width: 0;\r\n }\r\n\r\n .ad-title {\r\n overflow: hidden;\r\n text-overflow: ellipsis;\r\n white-space: nowrap;\r\n color: rgba(255, 255, 255, 0.82);\r\n font-size: 12.5px;\r\n line-height: 1.35;\r\n }\r\n\r\n .ad-meta {\r\n display: flex;\r\n align-items: center;\r\n gap: 6px;\r\n margin-top: 2px;\r\n font-size: 10.5px;\r\n line-height: 1.4;\r\n color: rgba(255, 255, 255, 0.45);\r\n }\r\n\r\n .ad-badge {\r\n flex: 0 0 auto;\r\n color: var(--ad-color, #6b727d);\r\n font-weight: 600;\r\n\r\n // 수동/훅으로 고정된 상태는 밑줄로 구분\r\n &.pinned {\r\n text-decoration: underline dotted;\r\n text-underline-offset: 2px;\r\n }\r\n }\r\n\r\n // 두 번째 줄 — 마지막으로 보낸 프롬프트\r\n .ad-prompt {\r\n display: flex;\r\n align-items: center;\r\n margin-top: 2px;\r\n min-width: 0;\r\n font-size: 11px;\r\n line-height: 1.4;\r\n }\r\n\r\n .ad-label {\r\n flex: 1 1 auto;\r\n overflow: hidden;\r\n text-overflow: ellipsis;\r\n white-space: nowrap;\r\n color: rgba(255, 255, 255, 0.6);\r\n\r\n // 아직 프롬프트가 없는 탭 — 자리는 지키되 눈에 덜 걸리게\r\n &.empty {\r\n color: rgba(255, 255, 255, 0.25);\r\n }\r\n }\r\n\r\n .ad-elapsed {\r\n flex: 0 0 auto;\r\n margin-left: auto;\r\n font-variant-numeric: tabular-nums;\r\n opacity: 0.55;\r\n }\r\n\r\n .ad-label-input {\r\n flex: 1 1 auto;\r\n min-width: 0;\r\n padding: 0 4px;\r\n border: 1px solid rgba(255, 255, 255, 0.25);\r\n border-radius: 3px;\r\n background: rgba(0, 0, 0, 0.35);\r\n color: #fff;\r\n font-size: 10.5px;\r\n outline: none;\r\n\r\n &:focus {\r\n border-color: var(--ad-color, #61afef);\r\n }\r\n }\r\n\r\n .ad-close {\r\n flex: 0 0 auto;\r\n align-self: center;\r\n width: 18px;\r\n height: 18px;\r\n padding: 0;\r\n border: 0;\r\n border-radius: 3px;\r\n opacity: 0;\r\n background: transparent;\r\n color: rgba(255, 255, 255, 0.8);\r\n font-size: 15px;\r\n line-height: 1;\r\n cursor: pointer;\r\n transition: opacity 0.1s ease, background 0.1s ease;\r\n\r\n &:hover {\r\n opacity: 1 !important;\r\n background: rgba(255, 80, 80, 0.35);\r\n }\r\n }\r\n\r\n .ad-foot {\r\n display: flex;\r\n gap: 6px;\r\n padding: 8px;\r\n border-top: 1px solid rgba(255, 255, 255, 0.07);\r\n }\r\n\r\n .ad-btn {\r\n flex: 1 1 auto;\r\n padding: 5px 8px;\r\n border: 1px solid rgba(255, 255, 255, 0.12);\r\n border-radius: 4px;\r\n background: transparent;\r\n color: rgba(255, 255, 255, 0.6);\r\n font-size: 11px;\r\n cursor: pointer;\r\n\r\n &:hover {\r\n background: rgba(255, 255, 255, 0.08);\r\n color: #fff;\r\n }\r\n }\r\n\r\n // 복구 버튼은 글자가 한 자뿐이라 나머지 버튼과 같은 비율로 늘어나면 허전하다\r\n .ad-repair {\r\n flex: 0 0 auto;\r\n min-width: 28px;\r\n font-size: 13px;\r\n line-height: 1;\r\n }\r\n}\r\n\r\n// 우클릭 상태 메뉴\r\n.ad-menu {\r\n position: fixed;\r\n z-index: 10000;\r\n min-width: 130px;\r\n padding: 4px;\r\n border: 1px solid rgba(255, 255, 255, 0.12);\r\n border-radius: 6px;\r\n background: #262a31;\r\n box-shadow: 0 6px 24px rgba(0, 0, 0, 0.5);\r\n font-size: 12px;\r\n user-select: none;\r\n\r\n .ad-menu-item {\r\n padding: 6px 10px;\r\n border-radius: 4px;\r\n color: rgba(255, 255, 255, 0.75);\r\n cursor: pointer;\r\n\r\n &:hover {\r\n background: rgba(255, 255, 255, 0.1);\r\n color: #fff;\r\n }\r\n }\r\n}\r\n\r\n// 도킹 가이드 — 헤더를 끌고 있는 동안만 창 위에 뜬다\r\n.ad-dock-overlay {\r\n position: fixed;\r\n inset: 0;\r\n z-index: 9998;\r\n // 커서 판정은 좌표로 직접 하므로 오버레이가 이벤트를 가로채면 안 된다\r\n pointer-events: none;\r\n}\r\n\r\n// 놓으면 사이드바가 자리잡을 영역\r\n.ad-dock-preview {\r\n position: fixed;\r\n display: none;\r\n border: 1px solid rgba(97, 175, 239, 0.9);\r\n background: rgba(97, 175, 239, 0.22);\r\n box-shadow: 0 0 0 9999px rgba(0, 0, 0, 0.18);\r\n transition: left 0.08s ease, top 0.08s ease, width 0.08s ease, height 0.08s ease;\r\n}\r\n\r\n// 네 가장자리에 뜨는 도킹 표적\r\n.ad-dock-zone {\r\n position: fixed;\r\n width: 46px;\r\n height: 46px;\r\n margin: -23px 0 0 -23px;\r\n border: 1px solid rgba(255, 255, 255, 0.35);\r\n border-radius: 6px;\r\n background: rgba(38, 42, 49, 0.92);\r\n box-shadow: 0 4px 16px rgba(0, 0, 0, 0.45);\r\n display: flex;\r\n align-items: center;\r\n justify-content: center;\r\n\r\n .ad-dock-mark {\r\n width: 26px;\r\n height: 26px;\r\n border: 1px solid rgba(255, 255, 255, 0.45);\r\n border-radius: 3px;\r\n }\r\n\r\n &.active {\r\n border-color: rgba(97, 175, 239, 0.95);\r\n background: rgba(97, 175, 239, 0.35);\r\n }\r\n\r\n // 표적 안의 작은 사각형이 \"어느 쪽 절반에 붙는지\" 를 그대로 보여준다\r\n &.ad-dock-zone-left .ad-dock-mark { border-left: 4px solid rgba(97, 175, 239, 0.95); }\r\n &.ad-dock-zone-right .ad-dock-mark { border-right: 4px solid rgba(97, 175, 239, 0.95); }\r\n &.ad-dock-zone-top .ad-dock-mark { border-top: 4px solid rgba(97, 175, 239, 0.95); }\r\n &.ad-dock-zone-bottom .ad-dock-mark { border-bottom: 4px solid rgba(97, 175, 239, 0.95); }\r\n}\r\n\r\n// 경계선을 끄는 동안에는 터미널이 커서를 뺏지 않게 한다\r\nbody.ad-resizing {\r\n cursor: inherit;\r\n\r\n .content.main {\r\n pointer-events: none;\r\n }\r\n}\r\n\r\n// AgentDeck 이 켜져 있을 때만 순정 탭바를 감춘다 (사이드바가 그 역할을 대신함)\r\nbody.agentdeck-active {\r\n .content.main > .tab-bar {\r\n display: none !important;\r\n }\r\n\r\n .window {\r\n display: flex;\r\n flex-direction: row;\r\n // 사이드바를 창 안에 절대 배치하므로 기준점이 필요하다\r\n position: relative;\r\n // 터미널 옆에 남는 여백이 밝은 기본 배경으로 튀지 않게 한다\r\n // (--ad-panel-bg 가 있으면 배경 이미지가 이 여백으로도 비친다)\r\n background: var(--ad-panel-bg, var(--bs-body-bg, #1e2127));\r\n }\r\n}\r\n"],"sourceRoot":""}]);
398
+ }`, "",{"version":3,"sources":["webpack://./src/styles.scss"],"names":[],"mappings":"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","sourcesContent":["// AgentDeck 사이드바 스타일\r\n// Tabby 테마 변수(--bs-*)를 우선 쓰되, 없을 때를 대비해 폴백 색을 함께 준다.\r\n\r\n// tabby-background 의 배경 이미지 레이어(.content-tab-active::before)를 터미널 영역으로 자른다.\r\n// 좌표 변수(--ad-bg-*)는 deck.service 의 syncBackgroundClip 이 relayout 마다 내려준다.\r\n// 그 플러그인은 런타임에 <style> 을 만들어 head 뒤쪽에 붙이므로 순서로는 못 이긴다 — !important 필수.\r\nbody.agentdeck-active.ad-bg-clip .content-tab-active::before {\r\n left: var(--ad-bg-left) !important;\r\n top: var(--ad-bg-top) !important;\r\n right: auto !important;\r\n bottom: auto !important;\r\n width: var(--ad-bg-width) !important;\r\n height: var(--ad-bg-height) !important;\r\n}\r\n\r\n#agentdeck-sidebar {\r\n display: flex;\r\n flex-direction: column;\r\n // 실제 위치와 크기는 deck.service 의 relayout 이 인라인으로 준다 (도킹 방향에 따라 달라짐)\r\n z-index: 5;\r\n overflow: hidden;\r\n // --ad-panel-bg 는 agentdeck 이 sidebarOpacity 로 내려준다 (배경 이미지가 비치게)\r\n background: var(--ad-panel-bg, var(--bs-body-bg, #1e2127));\r\n font-size: 12px;\r\n user-select: none;\r\n -webkit-app-region: no-drag;\r\n\r\n // 붙어 있는 쪽 반대편에만 경계선을 그린다\r\n &.ad-dock-right { border-left: 1px solid rgba(255, 255, 255, 0.08); }\r\n &.ad-dock-left { border-right: 1px solid rgba(255, 255, 255, 0.08); }\r\n &.ad-dock-top { border-bottom: 1px solid rgba(255, 255, 255, 0.08); }\r\n &.ad-dock-bottom { border-top: 1px solid rgba(255, 255, 255, 0.08); }\r\n\r\n // 상/하 도킹은 세로가 짧으므로 목록을 가로로 흘린다\r\n &.ad-dock-top,\r\n &.ad-dock-bottom {\r\n .ad-list {\r\n display: flex;\r\n flex-wrap: wrap;\r\n align-content: flex-start;\r\n gap: 0 6px;\r\n overflow-y: auto;\r\n }\r\n\r\n .ad-tab {\r\n flex: 0 0 220px;\r\n }\r\n }\r\n\r\n .ad-head {\r\n display: flex;\r\n align-items: center;\r\n justify-content: space-between;\r\n padding: 10px 12px 8px;\r\n letter-spacing: 0.12em;\r\n font-size: 10px;\r\n font-weight: 700;\r\n color: rgba(255, 255, 255, 0.4);\r\n // 헤더를 잡아 끌면 도킹 방향을 바꾼다 (OS 창 이동에 뺏기면 안 되므로 no-drag)\r\n -webkit-app-region: no-drag;\r\n cursor: grab;\r\n\r\n &:active {\r\n cursor: grabbing;\r\n }\r\n }\r\n\r\n // 경계선 — 잡아 끌면 크기가 바뀐다. 붙어 있는 쪽 반대편 가장자리에 놓는다\r\n .ad-resize {\r\n position: absolute;\r\n z-index: 6;\r\n background: transparent;\r\n transition: background 0.12s ease;\r\n\r\n &:hover,\r\n body.ad-resizing & {\r\n background: rgba(97, 175, 239, 0.5);\r\n }\r\n }\r\n\r\n .ad-resize-right,\r\n .ad-resize-left {\r\n top: 0;\r\n bottom: 0;\r\n width: 6px;\r\n cursor: col-resize;\r\n }\r\n\r\n .ad-resize-right { left: 0; }\r\n .ad-resize-left { right: 0; }\r\n\r\n .ad-resize-top,\r\n .ad-resize-bottom {\r\n left: 0;\r\n right: 0;\r\n height: 6px;\r\n cursor: row-resize;\r\n }\r\n\r\n .ad-resize-bottom { top: 0; }\r\n .ad-resize-top { bottom: 0; }\r\n\r\n // 상태별 집계 칩 한 줄 — `⏸ 2 ● 3 ○ 1`. 헤더 높이는 총 개수만 있던 때와 같게 유지한다\r\n .ad-head-count {\r\n -webkit-app-region: no-drag;\r\n display: flex;\r\n align-items: center;\r\n gap: 6px;\r\n flex: 0 0 auto;\r\n white-space: nowrap;\r\n letter-spacing: 0;\r\n color: rgba(255, 255, 255, 0.55);\r\n }\r\n\r\n .ad-cnt {\r\n padding: 1px 6px;\r\n border-radius: 8px;\r\n background: rgba(255, 255, 255, 0.08);\r\n color: var(--ad-color, rgba(255, 255, 255, 0.55));\r\n font-size: 10px;\r\n line-height: 1.3;\r\n font-variant-numeric: tabular-nums;\r\n }\r\n\r\n // 요구 2-1: 탭이 많아지면 이 영역만 스크롤된다\r\n .ad-list {\r\n flex: 1 1 auto;\r\n overflow-y: auto;\r\n overflow-x: hidden;\r\n padding: 2px 6px 8px;\r\n\r\n &::-webkit-scrollbar {\r\n width: 8px;\r\n }\r\n\r\n &::-webkit-scrollbar-thumb {\r\n background: rgba(255, 255, 255, 0.14);\r\n border-radius: 4px;\r\n\r\n &:hover {\r\n background: rgba(255, 255, 255, 0.24);\r\n }\r\n }\r\n }\r\n\r\n .ad-tab {\r\n position: relative;\r\n display: flex;\r\n align-items: stretch;\r\n gap: 8px;\r\n margin-bottom: 3px;\r\n padding: 7px 6px 7px 0;\r\n border-radius: 5px;\r\n cursor: pointer;\r\n transition: background 0.1s ease;\r\n\r\n &:hover {\r\n background: rgba(255, 255, 255, 0.05);\r\n\r\n .ad-close {\r\n opacity: 0.65;\r\n }\r\n }\r\n\r\n &.active {\r\n background: rgba(255, 255, 255, 0.1);\r\n\r\n .ad-title {\r\n color: #fff;\r\n }\r\n }\r\n }\r\n\r\n // 상태색 표시 바\r\n .ad-bar {\r\n flex: 0 0 3px;\r\n border-radius: 2px;\r\n background: var(--ad-color, #6b727d);\r\n }\r\n\r\n .ad-body {\r\n flex: 1 1 auto;\r\n min-width: 0;\r\n }\r\n\r\n .ad-title {\r\n overflow: hidden;\r\n text-overflow: ellipsis;\r\n white-space: nowrap;\r\n color: rgba(255, 255, 255, 0.82);\r\n font-size: 12.5px;\r\n line-height: 1.35;\r\n }\r\n\r\n .ad-meta {\r\n display: flex;\r\n align-items: center;\r\n gap: 6px;\r\n margin-top: 2px;\r\n font-size: 10.5px;\r\n line-height: 1.4;\r\n color: rgba(255, 255, 255, 0.45);\r\n }\r\n\r\n .ad-badge {\r\n // 이유가 붙으면(승인대기 · Bash 권한) 길어질 수 있다 — 경과시간을 밀어내지 않게 줄이고 말줄임\r\n flex: 0 1 auto;\r\n min-width: 0;\r\n max-width: 100%;\r\n overflow: hidden;\r\n text-overflow: ellipsis;\r\n white-space: nowrap;\r\n color: var(--ad-color, #6b727d);\r\n font-weight: 600;\r\n\r\n // 수동/훅으로 고정된 상태는 밑줄로 구분\r\n &.pinned {\r\n text-decoration: underline dotted;\r\n text-underline-offset: 2px;\r\n }\r\n }\r\n\r\n // 두 번째 줄 — 마지막으로 보낸 프롬프트\r\n .ad-prompt {\r\n display: flex;\r\n align-items: center;\r\n margin-top: 2px;\r\n min-width: 0;\r\n font-size: 11px;\r\n line-height: 1.4;\r\n }\r\n\r\n .ad-label {\r\n flex: 1 1 auto;\r\n overflow: hidden;\r\n text-overflow: ellipsis;\r\n white-space: nowrap;\r\n color: rgba(255, 255, 255, 0.6);\r\n\r\n // 아직 프롬프트가 없는 탭 — 자리는 지키되 눈에 덜 걸리게\r\n &.empty {\r\n color: rgba(255, 255, 255, 0.25);\r\n }\r\n }\r\n\r\n .ad-elapsed {\r\n flex: 0 0 auto;\r\n margin-left: auto;\r\n font-variant-numeric: tabular-nums;\r\n opacity: 0.55;\r\n }\r\n\r\n .ad-label-input {\r\n flex: 1 1 auto;\r\n min-width: 0;\r\n padding: 0 4px;\r\n border: 1px solid rgba(255, 255, 255, 0.25);\r\n border-radius: 3px;\r\n background: rgba(0, 0, 0, 0.35);\r\n color: #fff;\r\n font-size: 10.5px;\r\n outline: none;\r\n\r\n &:focus {\r\n border-color: var(--ad-color, #61afef);\r\n }\r\n }\r\n\r\n .ad-close {\r\n flex: 0 0 auto;\r\n align-self: center;\r\n width: 18px;\r\n height: 18px;\r\n padding: 0;\r\n border: 0;\r\n border-radius: 3px;\r\n opacity: 0;\r\n background: transparent;\r\n color: rgba(255, 255, 255, 0.8);\r\n font-size: 15px;\r\n line-height: 1;\r\n cursor: pointer;\r\n transition: opacity 0.1s ease, background 0.1s ease;\r\n\r\n &:hover {\r\n opacity: 1 !important;\r\n background: rgba(255, 80, 80, 0.35);\r\n }\r\n }\r\n\r\n .ad-foot {\r\n display: flex;\r\n gap: 6px;\r\n padding: 8px;\r\n border-top: 1px solid rgba(255, 255, 255, 0.07);\r\n }\r\n\r\n .ad-btn {\r\n flex: 1 1 auto;\r\n padding: 5px 8px;\r\n border: 1px solid rgba(255, 255, 255, 0.12);\r\n border-radius: 4px;\r\n background: transparent;\r\n color: rgba(255, 255, 255, 0.6);\r\n font-size: 11px;\r\n cursor: pointer;\r\n\r\n &:hover {\r\n background: rgba(255, 255, 255, 0.08);\r\n color: #fff;\r\n }\r\n }\r\n\r\n // 복구 버튼은 글자가 한 자뿐이라 나머지 버튼과 같은 비율로 늘어나면 허전하다\r\n .ad-repair {\r\n flex: 0 0 auto;\r\n min-width: 28px;\r\n font-size: 13px;\r\n line-height: 1;\r\n }\r\n}\r\n\r\n// 우클릭 상태 메뉴\r\n.ad-menu {\r\n position: fixed;\r\n z-index: 10000;\r\n min-width: 130px;\r\n padding: 4px;\r\n border: 1px solid rgba(255, 255, 255, 0.12);\r\n border-radius: 6px;\r\n background: #262a31;\r\n box-shadow: 0 6px 24px rgba(0, 0, 0, 0.5);\r\n font-size: 12px;\r\n user-select: none;\r\n\r\n .ad-menu-item {\r\n padding: 6px 10px;\r\n border-radius: 4px;\r\n color: rgba(255, 255, 255, 0.75);\r\n cursor: pointer;\r\n\r\n &:hover {\r\n background: rgba(255, 255, 255, 0.1);\r\n color: #fff;\r\n }\r\n }\r\n}\r\n\r\n// 도킹 가이드 — 헤더를 끌고 있는 동안만 창 위에 뜬다\r\n.ad-dock-overlay {\r\n position: fixed;\r\n inset: 0;\r\n z-index: 9998;\r\n // 커서 판정은 좌표로 직접 하므로 오버레이가 이벤트를 가로채면 안 된다\r\n pointer-events: none;\r\n}\r\n\r\n// 놓으면 사이드바가 자리잡을 영역\r\n.ad-dock-preview {\r\n position: fixed;\r\n display: none;\r\n border: 1px solid rgba(97, 175, 239, 0.9);\r\n background: rgba(97, 175, 239, 0.22);\r\n box-shadow: 0 0 0 9999px rgba(0, 0, 0, 0.18);\r\n transition: left 0.08s ease, top 0.08s ease, width 0.08s ease, height 0.08s ease;\r\n}\r\n\r\n// 네 가장자리에 뜨는 도킹 표적\r\n.ad-dock-zone {\r\n position: fixed;\r\n width: 46px;\r\n height: 46px;\r\n margin: -23px 0 0 -23px;\r\n border: 1px solid rgba(255, 255, 255, 0.35);\r\n border-radius: 6px;\r\n background: rgba(38, 42, 49, 0.92);\r\n box-shadow: 0 4px 16px rgba(0, 0, 0, 0.45);\r\n display: flex;\r\n align-items: center;\r\n justify-content: center;\r\n\r\n .ad-dock-mark {\r\n width: 26px;\r\n height: 26px;\r\n border: 1px solid rgba(255, 255, 255, 0.45);\r\n border-radius: 3px;\r\n }\r\n\r\n &.active {\r\n border-color: rgba(97, 175, 239, 0.95);\r\n background: rgba(97, 175, 239, 0.35);\r\n }\r\n\r\n // 표적 안의 작은 사각형이 \"어느 쪽 절반에 붙는지\" 를 그대로 보여준다\r\n &.ad-dock-zone-left .ad-dock-mark { border-left: 4px solid rgba(97, 175, 239, 0.95); }\r\n &.ad-dock-zone-right .ad-dock-mark { border-right: 4px solid rgba(97, 175, 239, 0.95); }\r\n &.ad-dock-zone-top .ad-dock-mark { border-top: 4px solid rgba(97, 175, 239, 0.95); }\r\n &.ad-dock-zone-bottom .ad-dock-mark { border-bottom: 4px solid rgba(97, 175, 239, 0.95); }\r\n}\r\n\r\n// 경계선을 끄는 동안에는 터미널이 커서를 뺏지 않게 한다\r\nbody.ad-resizing {\r\n cursor: inherit;\r\n\r\n .content.main {\r\n pointer-events: none;\r\n }\r\n}\r\n\r\n// AgentDeck 이 켜져 있을 때만 순정 탭바를 감춘다 (사이드바가 그 역할을 대신함)\r\nbody.agentdeck-active {\r\n .content.main > .tab-bar {\r\n display: none !important;\r\n }\r\n\r\n .window {\r\n display: flex;\r\n flex-direction: row;\r\n // 사이드바를 창 안에 절대 배치하므로 기준점이 필요하다\r\n position: relative;\r\n // 터미널 옆에 남는 여백이 밝은 기본 배경으로 튀지 않게 한다\r\n // (--ad-panel-bg 가 있으면 배경 이미지가 이 여백으로도 비친다)\r\n background: var(--ad-panel-bg, var(--bs-body-bg, #1e2127));\r\n }\r\n}\r\n"],"sourceRoot":""}]);
382
399
  // Exports
383
400
  /* harmony default export */ const __WEBPACK_DEFAULT_EXPORT__ = (___CSS_LOADER_EXPORT___);
384
401
 
@@ -823,6 +840,424 @@ function styleTagTransform(css, styleElement) {
823
840
  }
824
841
  module.exports = styleTagTransform;
825
842
 
843
+ /***/ },
844
+
845
+ /***/ "./src/alert.service.ts"
846
+ /*!******************************!*\
847
+ !*** ./src/alert.service.ts ***!
848
+ \******************************/
849
+ (__unused_webpack_module, __webpack_exports__, __webpack_require__) {
850
+
851
+ __webpack_require__.r(__webpack_exports__);
852
+ /* harmony export */ __webpack_require__.d(__webpack_exports__, {
853
+ /* harmony export */ AgentDeckAlertService: () => (/* binding */ AgentDeckAlertService)
854
+ /* harmony export */ });
855
+ /* harmony import */ var _angular_core__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! @angular/core */ "@angular/core");
856
+ /* harmony import */ var _angular_core__WEBPACK_IMPORTED_MODULE_0___default = /*#__PURE__*/__webpack_require__.n(_angular_core__WEBPACK_IMPORTED_MODULE_0__);
857
+ /* harmony import */ var tabby_core__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(/*! tabby-core */ "tabby-core");
858
+ /* harmony import */ var tabby_core__WEBPACK_IMPORTED_MODULE_1___default = /*#__PURE__*/__webpack_require__.n(tabby_core__WEBPACK_IMPORTED_MODULE_1__);
859
+ /* harmony import */ var fs__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(/*! fs */ "fs");
860
+ /* harmony import */ var fs__WEBPACK_IMPORTED_MODULE_2___default = /*#__PURE__*/__webpack_require__.n(fs__WEBPACK_IMPORTED_MODULE_2__);
861
+ /* harmony import */ var os__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(/*! os */ "os");
862
+ /* harmony import */ var os__WEBPACK_IMPORTED_MODULE_3___default = /*#__PURE__*/__webpack_require__.n(os__WEBPACK_IMPORTED_MODULE_3__);
863
+ /* harmony import */ var path__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(/*! path */ "path");
864
+ /* harmony import */ var path__WEBPACK_IMPORTED_MODULE_4___default = /*#__PURE__*/__webpack_require__.n(path__WEBPACK_IMPORTED_MODULE_4__);
865
+ /* harmony import */ var _status_service__WEBPACK_IMPORTED_MODULE_5__ = __webpack_require__(/*! ./status.service */ "./src/status.service.ts");
866
+ /* harmony import */ var _api__WEBPACK_IMPORTED_MODULE_6__ = __webpack_require__(/*! ./api */ "./src/api.ts");
867
+ var __decorate = (undefined && undefined.__decorate) || function (decorators, target, key, desc) {
868
+ var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
869
+ if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
870
+ 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;
871
+ return c > 3 && r && Object.defineProperty(target, key, r), r;
872
+ };
873
+ var __metadata = (undefined && undefined.__metadata) || function (k, v) {
874
+ if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
875
+ };
876
+
877
+
878
+
879
+
880
+
881
+
882
+
883
+ const DIAG_PATH = path__WEBPACK_IMPORTED_MODULE_4__.join(os__WEBPACK_IMPORTED_MODULE_3__.homedir(), '.agentdeck-diag.log');
884
+ /** 창 밖으로 알려야 하는 상태 — 사람이 손을 대야 진행되는 것들 */
885
+ /** 창이 뒤에 있을 때 알릴 상태 — 사람이 봐야 하는 것: 승인 / 오류 / 한도에 걸려 멈춤 */
886
+ const ALERT_STATUSES = ['waiting', 'error', 'limited'];
887
+ /**
888
+ * 창이 뒤로 가 있을 때의 OS 알림.
889
+ *
890
+ * 사이드바는 창이 보일 때만 의미가 있다. 에이전트 탭을 여섯 개쯤 굴리면서 Slack·VS 를 보고
891
+ * 있으면 어느 탭이 `승인대기` 로 멈춰 있어도 알 길이 없어서, 돌아와 보면 몇 분씩 놀고 있던
892
+ * 일이 잦았다. 그래서 상태가 **`waiting` / `error` 로 넘어가는 순간** 창이 포커스를 잃은
893
+ * 상태면 OS 쪽에 세 가지로 알린다.
894
+ *
895
+ * 1. **작업표시줄 깜빡임** (`flashFrame`) — 창이 포커스를 받으면 끈다. `alertFlash`
896
+ * 2. **작업표시줄 뱃지** — 지금 `waiting`+`error` 인 탭 수를 아이콘 위에 얹는다. Windows 에는
897
+ * `setBadgeCount` 가 없어서 `setOverlayIcon` 에 캔버스로 그린 원+숫자를 넣는다.
898
+ * 전이 순간만이 아니라 상태가 바뀔 때마다 갱신해 실제 개수를 따라간다. `alertBadge`
899
+ * 3. **OS 토스트** (`Notification`) — 기본은 끔. 누르면 창을 앞으로 가져오고 그 탭을 고른다. `alertToast`
900
+ *
901
+ * 창이 앞에 있을 때는 아무것도 하지 않는다 — 사이드바가 이미 보이고 있다.
902
+ * Electron 을 만지는 모든 곳은 try/catch 로 감싼다. 알림이 안 되는 것은 불편이지만
903
+ * 예외가 새어 나가 다른 배선을 죽이는 것은 사고다.
904
+ */
905
+ let AgentDeckAlertService = class AgentDeckAlertService {
906
+ constructor(app, config, zone, status) {
907
+ this.app = app;
908
+ this.config = config;
909
+ this.zone = zone;
910
+ this.status = status;
911
+ /** 탭별로 마지막에 본 상태 — "전이" 를 판정하려고 들고 있는다 */
912
+ this.last = new WeakMap();
913
+ /** flashFrame(true) 를 걸어 둔 상태인가 — 포커스가 오면 끈다 */
914
+ this.flashing = false;
915
+ /** 마지막으로 그린 뱃지 숫자 (0 = 안 그림). 같은 숫자면 다시 그리지 않는다 */
916
+ this.badgeShown = 0;
917
+ /** 뱃지 그림 캐시 — key 는 `${count}:${color}` */
918
+ this.badgeCache = new Map();
919
+ /**
920
+ * 진단/검증용 카운터 — CDP 에서 `__agentdeck.alert` 로 읽는다.
921
+ * `forceUnfocused` 를 true 로 두면 포커스 판정을 건너뛰고 "창이 뒤에 있다" 로 친다
922
+ * (테스트 인스턴스가 앞에 떠 있는 상태에서 알림 경로를 밟아 보려고)
923
+ */
924
+ this.debug = {
925
+ flashes: 0,
926
+ badgeCount: 0,
927
+ badgeDraws: 0,
928
+ toasts: 0,
929
+ transitions: 0,
930
+ lastFocused: null,
931
+ forceUnfocused: false,
932
+ };
933
+ }
934
+ init() {
935
+ this.app.ready$.subscribe(() => {
936
+ var _a;
937
+ try {
938
+ this.setup();
939
+ }
940
+ catch (e) {
941
+ this.diag(`alert setup ERROR ${(_a = e === null || e === void 0 ? void 0 : e.message) !== null && _a !== void 0 ? _a : e}`);
942
+ }
943
+ });
944
+ }
945
+ setup() {
946
+ var _a;
947
+ // 기동 시점의 상태를 기준선으로 삼는다 — 이미 waiting 인 탭은 "전이" 가 아니다
948
+ for (const tab of this.app.tabs) {
949
+ this.last.set(tab, this.status.get(tab).status);
950
+ }
951
+ this.status.changed$.subscribe(() => {
952
+ var _a;
953
+ try {
954
+ this.onChanged();
955
+ }
956
+ catch (e) {
957
+ this.diag(`alert onChanged ERROR ${(_a = e === null || e === void 0 ? void 0 : e.message) !== null && _a !== void 0 ? _a : e}`);
958
+ }
959
+ });
960
+ // 닫힌 탭은 개수에서 빠져야 한다 — status.changed$ 는 탭 닫힘에 안 울린다
961
+ this.app.tabClosed$.subscribe(tab => {
962
+ var _a;
963
+ this.last.delete(tab);
964
+ try {
965
+ this.updateBadge(this.countAlerting());
966
+ }
967
+ catch (e) {
968
+ this.diag(`alert tabClosed ERROR ${(_a = e === null || e === void 0 ? void 0 : e.message) !== null && _a !== void 0 ? _a : e}`);
969
+ }
970
+ });
971
+ // 사람이 돌아왔다 — 깜빡임은 여기서 끝난다. 뱃지는 상태가 풀릴 때 따로 지워진다
972
+ window.addEventListener('focus', () => this.stopFlash());
973
+ // 개발/진단용 — deck.service 가 만든 __agentdeck 에 붙는다 (없으면 만든다)
974
+ const g = window;
975
+ g.__agentdeck = (_a = g.__agentdeck) !== null && _a !== void 0 ? _a : {};
976
+ g.__agentdeck.alert = this.debug;
977
+ this.diag(`alert ready tabs=${this.app.tabs.length}`);
978
+ }
979
+ /**
980
+ * 상태가 바뀔 때마다 — 뱃지는 항상 실제 개수로 맞추고,
981
+ * 알림 상태로 **새로 들어온** 탭이 있고 창이 뒤에 있으면 깜빡임/토스트를 낸다
982
+ */
983
+ onChanged() {
984
+ const entered = [];
985
+ let count = 0;
986
+ for (const tab of this.app.tabs) {
987
+ const now = this.status.get(tab).status;
988
+ const prev = this.last.get(tab);
989
+ this.last.set(tab, now);
990
+ if (!ALERT_STATUSES.includes(now)) {
991
+ continue;
992
+ }
993
+ count++;
994
+ // waiting -> error 도 전이로 친다 — 성격이 다른 일이 새로 생긴 것이다
995
+ if (prev !== now) {
996
+ entered.push(tab);
997
+ }
998
+ }
999
+ this.updateBadge(count);
1000
+ if (entered.length === 0) {
1001
+ return;
1002
+ }
1003
+ this.debug.transitions += entered.length;
1004
+ const focused = this.isFocused();
1005
+ this.debug.lastFocused = focused;
1006
+ if (focused) {
1007
+ return;
1008
+ }
1009
+ this.diag(`alert enter=${entered.length} count=${count} focused=${focused}`);
1010
+ if (this.config.store.agentDeck.alertFlash) {
1011
+ this.flash();
1012
+ }
1013
+ if (this.config.store.agentDeck.alertToast) {
1014
+ for (const tab of entered) {
1015
+ this.toast(tab);
1016
+ }
1017
+ }
1018
+ }
1019
+ countAlerting() {
1020
+ let n = 0;
1021
+ for (const tab of this.app.tabs) {
1022
+ if (ALERT_STATUSES.includes(this.status.get(tab).status)) {
1023
+ n++;
1024
+ }
1025
+ }
1026
+ return n;
1027
+ }
1028
+ // ---------- Electron 접근 ----------
1029
+ /** `@electron/remote` 의 현재 창. 못 잡으면 null — 호출부는 전부 null 을 감안한다 */
1030
+ win() {
1031
+ var _a, _b, _c, _d;
1032
+ try {
1033
+ const req = (_a = window.require) !== null && _a !== void 0 ? _a : globalThis.require;
1034
+ return (_d = (_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)) !== null && _d !== void 0 ? _d : null;
1035
+ }
1036
+ catch {
1037
+ return null;
1038
+ }
1039
+ }
1040
+ nativeImage() {
1041
+ var _a, _b, _c;
1042
+ try {
1043
+ const req = (_a = window.require) !== null && _a !== void 0 ? _a : globalThis.require;
1044
+ return (_c = (_b = req === null || req === void 0 ? void 0 : req('electron')) === null || _b === void 0 ? void 0 : _b.nativeImage) !== null && _c !== void 0 ? _c : null;
1045
+ }
1046
+ catch {
1047
+ return null;
1048
+ }
1049
+ }
1050
+ /**
1051
+ * 창이 앞에 있나. Electron 의 판정을 우선하고(devtools 를 따로 띄워도 본창 기준으로 본다),
1052
+ * remote 를 못 잡으면 DOM 의 `document.hasFocus()` 로 대신한다
1053
+ */
1054
+ isFocused() {
1055
+ if (this.debug.forceUnfocused) {
1056
+ return false;
1057
+ }
1058
+ try {
1059
+ const win = this.win();
1060
+ if (win === null || win === void 0 ? void 0 : win.isFocused) {
1061
+ return !!win.isFocused();
1062
+ }
1063
+ }
1064
+ catch {
1065
+ // 아래 DOM 판정으로
1066
+ }
1067
+ return document.hasFocus();
1068
+ }
1069
+ // ---------- 깜빡임 ----------
1070
+ flash() {
1071
+ var _a;
1072
+ try {
1073
+ const win = this.win();
1074
+ if (!(win === null || win === void 0 ? void 0 : win.flashFrame)) {
1075
+ return;
1076
+ }
1077
+ win.flashFrame(true);
1078
+ this.flashing = true;
1079
+ this.debug.flashes++;
1080
+ }
1081
+ catch (e) {
1082
+ this.diag(`alert flash ERROR ${(_a = e === null || e === void 0 ? void 0 : e.message) !== null && _a !== void 0 ? _a : e}`);
1083
+ }
1084
+ }
1085
+ stopFlash() {
1086
+ var _a, _b;
1087
+ if (!this.flashing) {
1088
+ return;
1089
+ }
1090
+ this.flashing = false;
1091
+ try {
1092
+ (_b = (_a = this.win()) === null || _a === void 0 ? void 0 : _a.flashFrame) === null || _b === void 0 ? void 0 : _b.call(_a, false);
1093
+ }
1094
+ catch {
1095
+ // 포커스가 오면 Windows 가 알아서 멈추므로 실패해도 남는 건 없다
1096
+ }
1097
+ }
1098
+ // ---------- 뱃지 ----------
1099
+ /**
1100
+ * 작업표시줄 아이콘 위의 숫자를 지금 개수로 맞춘다. 0 이면 지운다.
1101
+ * `alertBadge` 가 꺼져 있으면 그려 둔 것이 있을 때만 지우고 끝낸다
1102
+ */
1103
+ updateBadge(count) {
1104
+ var _a;
1105
+ this.debug.badgeCount = count;
1106
+ const enabled = !!this.config.store.agentDeck.alertBadge;
1107
+ const want = enabled ? count : 0;
1108
+ if (want === this.badgeShown) {
1109
+ return;
1110
+ }
1111
+ try {
1112
+ const win = this.win();
1113
+ if (!(win === null || win === void 0 ? void 0 : win.setOverlayIcon)) {
1114
+ return;
1115
+ }
1116
+ if (want === 0) {
1117
+ win.setOverlayIcon(null, '');
1118
+ this.badgeShown = 0;
1119
+ // 개수가 0 으로 내려왔으면 깜빡일 이유도 사라졌다
1120
+ this.stopFlash();
1121
+ return;
1122
+ }
1123
+ const img = this.badgeImage(want);
1124
+ if (!img) {
1125
+ return;
1126
+ }
1127
+ win.setOverlayIcon(img, `승인대기/오류 ${want}개`);
1128
+ this.badgeShown = want;
1129
+ this.debug.badgeDraws++;
1130
+ }
1131
+ catch (e) {
1132
+ this.diag(`alert badge ERROR ${(_a = e === null || e === void 0 ? void 0 : e.message) !== null && _a !== void 0 ? _a : e}`);
1133
+ }
1134
+ }
1135
+ /**
1136
+ * 원 위에 숫자를 얹은 오버레이 이미지. 오류 탭이 하나라도 있으면 오류색, 아니면 승인대기색.
1137
+ * Windows 오버레이는 16px 기준이라 16@1x + 32@2x 두 표현을 넣는다 (고배율에서 흐려지지 않게).
1138
+ * `addRepresentation` 을 못 쓰는 빌드면 32px 하나로 물러난다
1139
+ */
1140
+ badgeImage(count) {
1141
+ var _a;
1142
+ const hasError = this.app.tabs.some(t => this.status.get(t).status === 'error');
1143
+ const color = hasError ? _api__WEBPACK_IMPORTED_MODULE_6__.STATUS_STYLES.error.color : _api__WEBPACK_IMPORTED_MODULE_6__.STATUS_STYLES.waiting.color;
1144
+ const key = `${count}:${color}`;
1145
+ const cached = this.badgeCache.get(key);
1146
+ if (cached) {
1147
+ return cached;
1148
+ }
1149
+ const nativeImage = this.nativeImage();
1150
+ if (!nativeImage) {
1151
+ return null;
1152
+ }
1153
+ const text = count > 9 ? '9+' : String(count);
1154
+ let img;
1155
+ try {
1156
+ img = nativeImage.createEmpty();
1157
+ img.addRepresentation({ scaleFactor: 1, width: 16, height: 16, dataURL: this.drawBadge(16, text, color) });
1158
+ img.addRepresentation({ scaleFactor: 2, width: 32, height: 32, dataURL: this.drawBadge(32, text, color) });
1159
+ if ((_a = img.isEmpty) === null || _a === void 0 ? void 0 : _a.call(img)) {
1160
+ throw new Error('empty after addRepresentation');
1161
+ }
1162
+ }
1163
+ catch {
1164
+ img = nativeImage.createFromDataURL(this.drawBadge(32, text, color));
1165
+ }
1166
+ this.badgeCache.set(key, img);
1167
+ return img;
1168
+ }
1169
+ /** 오프스크린 canvas 에 원+숫자를 그려 data URL 로 돌려준다 */
1170
+ drawBadge(size, text, color) {
1171
+ const canvas = document.createElement('canvas');
1172
+ canvas.width = size;
1173
+ canvas.height = size;
1174
+ const ctx = canvas.getContext('2d');
1175
+ if (!ctx) {
1176
+ throw new Error('canvas 2d 미지원');
1177
+ }
1178
+ const r = size / 2;
1179
+ ctx.clearRect(0, 0, size, size);
1180
+ ctx.beginPath();
1181
+ ctx.arc(r, r, r - 0.5, 0, Math.PI * 2);
1182
+ ctx.fillStyle = color;
1183
+ ctx.fill();
1184
+ // 어두운 작업표시줄 위에서 원 테두리가 살게 얇은 흰 선
1185
+ ctx.lineWidth = Math.max(1, size / 16);
1186
+ ctx.strokeStyle = 'rgba(255,255,255,0.85)';
1187
+ ctx.stroke();
1188
+ ctx.fillStyle = '#ffffff';
1189
+ ctx.font = `bold ${Math.round(size * (text.length > 1 ? 0.55 : 0.7))}px "Segoe UI", sans-serif`;
1190
+ ctx.textAlign = 'center';
1191
+ ctx.textBaseline = 'middle';
1192
+ // 글리프의 시각적 중심이 살짝 위로 치우쳐서 조금 내린다
1193
+ ctx.fillText(text, r, r + size * 0.04);
1194
+ return canvas.toDataURL('image/png');
1195
+ }
1196
+ // ---------- 토스트 ----------
1197
+ /** OS 토스트. 누르면 창을 앞으로 가져오고 그 탭을 고른다 */
1198
+ toast(tab) {
1199
+ var _a;
1200
+ try {
1201
+ if (typeof Notification === 'undefined') {
1202
+ return;
1203
+ }
1204
+ const s = this.status.get(tab);
1205
+ const name = s.label || tab.title || '(제목 없음)';
1206
+ const style = _api__WEBPACK_IMPORTED_MODULE_6__.STATUS_STYLES[s.status];
1207
+ const n = new Notification('AgentDeck', {
1208
+ body: `${name} — ${style.label}`,
1209
+ // 같은 탭의 토스트는 하나로 겹친다 (waiting -> error 가 잇달아 와도 두 장이 쌓이지 않게)
1210
+ tag: `agentdeck-${this.tabKey(tab)}`,
1211
+ });
1212
+ this.debug.toasts++;
1213
+ n.onclick = () => {
1214
+ var _a, _b;
1215
+ try {
1216
+ const win = this.win();
1217
+ (_a = win === null || win === void 0 ? void 0 : win.show) === null || _a === void 0 ? void 0 : _a.call(win);
1218
+ (_b = win === null || win === void 0 ? void 0 : win.focus) === null || _b === void 0 ? void 0 : _b.call(win);
1219
+ }
1220
+ catch {
1221
+ // 창을 못 올려도 탭 선택은 해 둔다
1222
+ }
1223
+ this.zone.run(() => {
1224
+ if (this.app.tabs.includes(tab)) {
1225
+ this.app.selectTab(tab);
1226
+ }
1227
+ });
1228
+ n.close();
1229
+ };
1230
+ }
1231
+ catch (e) {
1232
+ this.diag(`alert toast ERROR ${(_a = e === null || e === void 0 ? void 0 : e.message) !== null && _a !== void 0 ? _a : e}`);
1233
+ }
1234
+ }
1235
+ /** 토스트 tag 용 탭 식별자 — 객체 자체는 문자열이 안 되니 인덱스로 대신한다 */
1236
+ tabKey(tab) {
1237
+ const i = this.app.tabs.indexOf(tab);
1238
+ return i >= 0 ? String(i) : 'x';
1239
+ }
1240
+ // ---------- 진단 ----------
1241
+ /** deck.service 와 같은 파일(`~/.agentdeck-diag.log`)에 남긴다. 실패해도 동작에는 영향 없다 */
1242
+ diag(line) {
1243
+ try {
1244
+ fs__WEBPACK_IMPORTED_MODULE_2__.appendFileSync(DIAG_PATH, `${new Date().toISOString()} ${line}\n`);
1245
+ }
1246
+ catch {
1247
+ console.debug('[agentdeck]', line);
1248
+ }
1249
+ }
1250
+ };
1251
+ AgentDeckAlertService = __decorate([
1252
+ (0,_angular_core__WEBPACK_IMPORTED_MODULE_0__.Injectable)({ providedIn: 'root' }),
1253
+ __metadata("design:paramtypes", [tabby_core__WEBPACK_IMPORTED_MODULE_1__.AppService,
1254
+ tabby_core__WEBPACK_IMPORTED_MODULE_1__.ConfigService,
1255
+ _angular_core__WEBPACK_IMPORTED_MODULE_0__.NgZone,
1256
+ _status_service__WEBPACK_IMPORTED_MODULE_5__.WorkStatusService])
1257
+ ], AgentDeckAlertService);
1258
+
1259
+
1260
+
826
1261
  /***/ },
827
1262
 
828
1263
  /***/ "./src/api.ts"
@@ -839,12 +1274,47 @@ __webpack_require__.r(__webpack_exports__);
839
1274
  const STATUS_STYLES = {
840
1275
  running: { icon: '●', label: '진행중', color: '#e5c07b' },
841
1276
  waiting: { icon: '⏸', label: '승인대기', color: '#61afef' },
1277
+ limited: { icon: '⛔', label: '한도 도달', color: '#c678dd' },
842
1278
  done: { icon: '✔', label: '완료', color: '#98c379' },
843
1279
  error: { icon: '✖', label: '오류', color: '#e06c75' },
844
1280
  idle: { icon: '○', label: '대기', color: '#6b727d' },
845
1281
  };
846
1282
 
847
1283
 
1284
+ /***/ },
1285
+
1286
+ /***/ "./src/bind.ts"
1287
+ /*!*********************!*\
1288
+ !*** ./src/bind.ts ***!
1289
+ \*********************/
1290
+ (__unused_webpack_module, __webpack_exports__, __webpack_require__) {
1291
+
1292
+ __webpack_require__.r(__webpack_exports__);
1293
+ /* harmony export */ __webpack_require__.d(__webpack_exports__, {
1294
+ /* harmony export */ pickTabByPids: () => (/* binding */ pickTabByPids)
1295
+ /* harmony export */ });
1296
+ function pickTabByPids(pids, tabPids) {
1297
+ const wanted = (pids !== null && pids !== void 0 ? pids : []).filter(n => Number.isInteger(n) && n > 0);
1298
+ let anyInfo = false;
1299
+ for (const list of tabPids.values()) {
1300
+ if (list.length) {
1301
+ anyInfo = true;
1302
+ break;
1303
+ }
1304
+ }
1305
+ if (!wanted.length || !anyInfo) {
1306
+ return { tab: null, mode: 'legacy' };
1307
+ }
1308
+ const set = new Set(wanted);
1309
+ for (const [tab, list] of tabPids) {
1310
+ if (list.some(p => set.has(p))) {
1311
+ return { tab, mode: 'pid' };
1312
+ }
1313
+ }
1314
+ return { tab: null, mode: 'pid-miss' };
1315
+ }
1316
+
1317
+
848
1318
  /***/ },
849
1319
 
850
1320
  /***/ "./src/claudeHooks.ts"
@@ -901,8 +1371,16 @@ const PLAN = [
901
1371
  { event: 'UserPromptSubmit', args: '-Status running' },
902
1372
  // 승인을 기다린다
903
1373
  { event: 'Notification', args: '-Status waiting' },
1374
+ // 승인 뒤 도구가 돌았다 / 실패했다 / 사람이 거부했다 — 어느 쪽이든 에이전트는 다시 일하는 중이다.
1375
+ // 이 셋이 없으면 승인대기에서 내려올 훅이 Stop 밖에 없어 승인을 해 줘도 사이드바가 승인대기에
1376
+ // 박혀 있었다 (2026-09-07 실측). 스크립트는 이미 running 이면 아무것도 쓰지 않아 도구마다 도는 비용이 없다
1377
+ { event: 'PostToolUse', args: '-Status running' },
1378
+ { event: 'PostToolUseFailure', args: '-Status running' },
1379
+ { event: 'PermissionDenied', args: '-Status running' },
904
1380
  // 답이 끝났다
905
1381
  { event: 'Stop', args: '-Status done' },
1382
+ // 턴이 실패로 끝났다 — 스크립트가 `error` 를 보고 rate_limit(사용량 한도)이면 limited 로 바꿔 보낸다
1383
+ { event: 'StopFailure', args: '-Status error' },
906
1384
  ];
907
1385
  function claudeSettingsPath() {
908
1386
  return path__WEBPACK_IMPORTED_MODULE_2__.join(os__WEBPACK_IMPORTED_MODULE_1__.homedir(), '.claude', 'settings.json');
@@ -931,7 +1409,8 @@ function withoutOurs(entries) {
931
1409
  .filter(e => !JSON.stringify(e !== null && e !== void 0 ? e : '').includes(MARK));
932
1410
  }
933
1411
  /**
934
- * 우리 훅이 세 이벤트에 모두 걸려 있나 — 파일을 읽지 않는 순수 판정.
1412
+ * 우리 훅이 PLAN 의 모든 이벤트에 걸려 있나 — 파일을 읽지 않는 순수 판정.
1413
+ * 이벤트가 늘어난 버전으로 올라오면 옛 설치는 "설치 안 됨" 으로 보인다 — 설정 창에서 설치를 다시 누르면 된다.
935
1414
  * 파일 접근과 분리해 둔 이유는 이 판단을 테스트로 묶어 두기 위해서다.
936
1415
  */
937
1416
  function isMerged(settings) {
@@ -1084,12 +1563,26 @@ class AgentDeckConfigProvider extends tabby_core__WEBPACK_IMPORTED_MODULE_0__.Co
1084
1563
  * 설정 창의 설치 버튼은 이 값과 무관하게 언제든 쓸 수 있다.
1085
1564
  */
1086
1565
  hookPromptDismissed: false,
1566
+ /**
1567
+ * 기동 시 지난 탭을 복원하지 않는다 (Tabby `recoverTabs` 를 false 로 강제).
1568
+ *
1569
+ * 복원되는 건 셸뿐이고 그 안의 Claude Code 세션은 이미 죽어 있어 의미가 없다.
1570
+ * 켜면 항상 빈 상태(기본 프로필 새 탭)로 시작하고, 끄면 Tabby 순정 동작으로 돌아간다.
1571
+ * 설정 창에서 토글한다 (recovery.service.ts).
1572
+ */
1573
+ noTabRecovery: false,
1087
1574
  /** PTY 출력이 이 시간(ms) 이상 없으면 running -> idle */
1088
1575
  idleAfterMs: 2500,
1089
1576
  /** 출력 패턴으로 상태를 자동 추론할지 */
1090
1577
  autoDetect: true,
1091
1578
  /** 사이드바에 경과 시간 표시 */
1092
1579
  showElapsed: true,
1580
+ /**
1581
+ * 사이드바 탭을 상태 우선순위(waiting → error → running → done → idle)로 정렬한다.
1582
+ * 같은 상태끼리는 탭 순서를 지킨다. 기본은 끔 — 순정 탭바와 같은 탭 순서로 보인다.
1583
+ * 헤더의 상태별 집계는 이 값과 무관하게 항상 보인다.
1584
+ */
1585
+ sortByStatus: false,
1093
1586
  /**
1094
1587
  * 창이 가려져 있어도 사이드바를 계속 갱신한다 (Electron backgroundThrottling 해제).
1095
1588
  *
@@ -1118,6 +1611,22 @@ class AgentDeckConfigProvider extends tabby_core__WEBPACK_IMPORTED_MODULE_0__.Co
1118
1611
  * false 로 두면 터미널 앱에 리터럴 Ctrl+V(0x16, quoted-insert)를 보낼 수 있다
1119
1612
  */
1120
1613
  claimCtrlV: true,
1614
+ /**
1615
+ * Shift+Enter / Ctrl+Enter 를 캡처 단계에서 직접 줄바꿈으로 처리한다.
1616
+ *
1617
+ * 핫키(agentdeck-newline)에만 맡기면 한글 조합 중에 눌렀을 때 Shift 가 실리지
1618
+ * 않은 Enter 로 도착해 전송돼 버린다 (deck.service.ts `claimShiftEnterKey` 주석).
1619
+ * false 로 두면 예전처럼 핫키 경로만 쓴다.
1620
+ */
1621
+ claimShiftEnter: true,
1622
+ /**
1623
+ * Home / End 를 Tabby 핫키에서 걷어내 xterm 순정 경로로 돌려준다.
1624
+ *
1625
+ * 핫키로 두면 한글 조합 중에 눌렀을 때 조합 음절이 커서를 따라간다
1626
+ * (deck.service.ts `releaseHomeEndHotkey` 주석에 원인). 방향키가 멀쩡한 이유도 같다 —
1627
+ * 방향키는 핫키가 아니라서 xterm 이 조합을 먼저 확정하고 키를 보낸다.
1628
+ */
1629
+ releaseHomeEnd: true,
1121
1630
  /**
1122
1631
  * 클립보드에 이미지만 있으면 Ctrl+V 로도 이미지가 들어가게 한다.
1123
1632
  *
@@ -1303,6 +1812,23 @@ class AgentDeckConfigProvider extends tabby_core__WEBPACK_IMPORTED_MODULE_0__.Co
1303
1812
  },
1304
1813
  /** 기본 프로필 지정을 이미 했는지 — 사용자 선택을 매번 덮어쓰지 않기 위한 플래그 */
1305
1814
  rootProfileClaimed: false,
1815
+ /**
1816
+ * 창이 뒤에 있을 때(포커스 없음) 탭이 `승인대기`/`오류` 로 넘어가면 OS 쪽에 알린다.
1817
+ * 창이 앞에 있으면 세 항목 모두 아무 일도 하지 않는다 — 사이드바가 이미 보이고 있다.
1818
+ * 작업표시줄 아이콘을 깜빡인다(`flashFrame`). 창이 포커스를 받으면 멈춘다
1819
+ */
1820
+ alertFlash: true,
1821
+ /**
1822
+ * 작업표시줄 아이콘 위에 `승인대기`+`오류` 탭 개수를 뱃지로 얹는다.
1823
+ * Windows 는 `setBadgeCount` 가 없어 오버레이 아이콘(원+숫자)으로 그린다.
1824
+ * 이 값은 전이 순간이 아니라 상태가 바뀔 때마다 실제 개수로 갱신되고, 0 이면 지운다
1825
+ */
1826
+ alertBadge: true,
1827
+ /**
1828
+ * OS 토스트(`Notification`)까지 띄운다. 기본은 끔 — 탭이 여럿이면 시끄럽다.
1829
+ * 토스트를 누르면 창을 앞으로 가져오고 그 탭을 고른다
1830
+ */
1831
+ alertToast: false,
1306
1832
  },
1307
1833
  hotkeys: {
1308
1834
  'agentdeck-toggle': [],
@@ -1352,6 +1878,7 @@ __webpack_require__.r(__webpack_exports__);
1352
1878
  /* harmony import */ var _prompt__WEBPACK_IMPORTED_MODULE_9__ = __webpack_require__(/*! ./prompt */ "./src/prompt.ts");
1353
1879
  /* harmony import */ var _screen__WEBPACK_IMPORTED_MODULE_10__ = __webpack_require__(/*! ./screen */ "./src/screen.ts");
1354
1880
  /* harmony import */ var _dock__WEBPACK_IMPORTED_MODULE_11__ = __webpack_require__(/*! ./dock */ "./src/dock.ts");
1881
+ /* harmony import */ var _order__WEBPACK_IMPORTED_MODULE_12__ = __webpack_require__(/*! ./order */ "./src/order.ts");
1355
1882
  var __decorate = (undefined && undefined.__decorate) || function (decorators, target, key, desc) {
1356
1883
  var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
1357
1884
  if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
@@ -1373,6 +1900,7 @@ var __metadata = (undefined && undefined.__metadata) || function (k, v) {
1373
1900
 
1374
1901
 
1375
1902
 
1903
+
1376
1904
  const SIDEBAR_ID = 'agentdeck-sidebar';
1377
1905
  const BODY_CLASS = 'agentdeck-active';
1378
1906
  /** 배경 이미지 레이어를 터미널 영역으로 자르는 중일 때 body 에 붙는 클래스 (styles.scss 참조) */
@@ -1528,6 +2056,8 @@ let AgentDeckService = class AgentDeckService {
1528
2056
  this.observer = null;
1529
2057
  this.editing = null;
1530
2058
  this.renderQueued = false;
2059
+ /** Shift 물리 키가 눌려 있나 — IME 조합을 거친 Enter 는 shiftKey 가 지워져 온다 */
2060
+ this.shiftDownAt = 0;
1531
2061
  /** rAF 가 멈춘 상황(창 가려짐)을 대비한 렌더 폴백 타이머 */
1532
2062
  this.renderTimer = null;
1533
2063
  /** 직전에 적용한 폭 — 같은 값이면 DOM 을 다시 건드리지 않는다 */
@@ -1621,7 +2151,9 @@ let AgentDeckService = class AgentDeckService {
1621
2151
  this.disableBackgroundThrottling();
1622
2152
  this.ensurePasteHotkey();
1623
2153
  this.ensureNewlineHotkey();
2154
+ this.releaseHomeEndHotkey();
1624
2155
  this.claimCtrlVKey();
2156
+ this.claimShiftEnterKey();
1625
2157
  this.claimEnterLabel();
1626
2158
  this.claimRightClick();
1627
2159
  // xterm 의 네이티브 paste 를 관측만 한다 (doPaste 의 이중 입력 방지용).
@@ -2302,30 +2834,148 @@ let AgentDeckService = class AgentDeckService {
2302
2834
  }, true);
2303
2835
  }
2304
2836
  /**
2305
- * Enter 를 눌러 프롬프트를 보낼 때, 그 프롬프트 원문을 작업 라벨로 삼는다.
2837
+ * Home / End 를 Tabby 핫키 테이블에서 걷어내 xterm 순정 경로로 돌려준다.
2306
2838
  *
2307
- * 왜 이게 필요한가 — 라벨을 채우는 다른 경로(훅·콘솔 제목)는 둘 다 배포판에서 못 믿는다.
2308
- * 훅은 사용자가 `~/.claude/settings.json` 을 손봐야 돌고, 플러그인이 그걸 대신 깔아줄 수도 없다.
2309
- * 콘솔 제목은 Claude Code 가 세션 첫 작업을 요약해 한 번 박고 그 뒤로 갱신하지 않는다
2310
- * (2026-08-28 실측: 작업이 네 번 바뀌는 동안 `◑ sendInput 한 줄 vs 두 줄 원인 파악` 그대로).
2311
- * 그래서 설정이 하나도 없는 상태에서 라벨을 최신으로 유지할 수 있는 건 이 경로뿐이다.
2839
+ * 증상: 한글 마지막 음절이 조합 중일 때 Home/End 를 누르면 그 음절이 **커서를 따라간다**
2840
+ * (2026-09-08 사용자 보고). 방향키는 멀쩡하다 — 그 차이가 곧 원인이다.
2312
2841
  *
2313
- * **Enter 만 잡는다.** Shift+Enter / Ctrl+Enter 는 줄바꿈이라(agentdeck-newline) 아직 전송이
2314
- * 아니고, 그때 라벨을 바꾸면 쓰다 만 문장이 박힌다. 수식키가 하나라도 눌려 있으면 그냥 흘린다.
2315
- * IME 조합 중의 Enter(한글 확정)도 전송이 아니므로 제외한다.
2842
+ * 방향키는 핫키가 아니라서 xterm 의 키 경로를 그대로 탄다. 거기서는 조합 중에 아무 키나
2843
+ * 들어오면 `CompositionHelper.keydown` 이 `_finalizeComposition(false)` 로 조합을
2844
+ * **동기 전송**하고(`tabby-terminal/dist/index.js:44098`, `:44120` triggerDataEvent) 그다음
2845
+ * 방향키를 보낸다 — 글자 먼저, 이동 나중이라 순서가 맞는다.
2316
2846
  *
2317
- * 관측만 하고 이벤트는 절대 막지 않는다 — 여기서 preventDefault 하면 프롬프트가 안 보내진다.
2847
+ * Home/End 는 그 경로를 못 탄다. Tabby 가 `hotkeys.home` / `hotkeys.end` 로 선점해서
2848
+ * `tabby-local/dist/index.js:613` 이 `sendInput('')` 를 **즉시** 쏘기 때문이다.
2849
+ * xterm 을 거치지 않으니 조합 확정은 `compositionend` 경로로 밀리는데, 그쪽
2850
+ * `_finalizeComposition(true)` 는 `setTimeout(..., 0)` 로 **비동기 전송**이다
2851
+ * (`tabby-terminal/dist/index.js:44110`). 결국 커서 이동이 먼저 나가고 음절이 한 틱 뒤에
2852
+ * 도착한다 — 앱은 옮겨간 자리에 글자를 찍는다.
2853
+ *
2854
+ * 그래서 고치는 방법은 핸들러를 더 얹는 게 아니라 **핫키를 놓아주는 것**이다. 비워두면
2855
+ * xterm 이 Home/End 를 방향키와 똑같이 처리한다(조합 확정 → 키 전송). 되돌리려면
2856
+ * `agentDeck.releaseHomeEnd: false` 후 설정에서 다시 지정하면 된다.
2318
2857
  */
2319
- claimEnterLabel() {
2320
- document.addEventListener('keydown', (event) => {
2321
- if (!this.config.store.agentDeck.enterAsLabel) {
2322
- return;
2323
- }
2324
- if (event.shiftKey || event.ctrlKey || event.altKey || event.metaKey) {
2325
- return; // 줄바꿈이지 전송이 아니다
2326
- }
2327
- if (event.key !== 'Enter' && event.code !== 'Enter' && event.code !== 'NumpadEnter') {
2328
- return;
2858
+ releaseHomeEndHotkey() {
2859
+ if (!this.config.store.agentDeck.releaseHomeEnd) {
2860
+ return;
2861
+ }
2862
+ const hotkeys = this.config.store.hotkeys;
2863
+ if (!hotkeys) {
2864
+ return;
2865
+ }
2866
+ let changed = false;
2867
+ for (const id of ['home', 'end']) {
2868
+ const keys = Array.isArray(hotkeys[id]) ? hotkeys[id] : [];
2869
+ const kept = keys.filter(k => k !== 'Home' && k !== 'End');
2870
+ if (kept.length !== keys.length) {
2871
+ hotkeys[id] = kept;
2872
+ changed = true;
2873
+ }
2874
+ }
2875
+ if (changed) {
2876
+ this.config.save();
2877
+ }
2878
+ this.diag(`${new Date().toISOString()} hotkeys home=${JSON.stringify(hotkeys.home)} `
2879
+ + `end=${JSON.stringify(hotkeys.end)}`);
2880
+ }
2881
+ /**
2882
+ * Shift+Enter / Ctrl+Enter 를 document **캡처 단계**에서 직접 줄바꿈으로 바꾼다.
2883
+ *
2884
+ * 핫키(`agentdeck-newline`)에만 맡겼더니 한글을 치다 누르면 절반쯤 줄바꿈이 아니라
2885
+ * **전송**이 됐다 (2026-09-08 사용자 보고: "50% 정도는 그냥 enter 로 들어간다").
2886
+ * 진단로그에 증거가 남아 있다 — 오발신 순간 `enter-label` 이 발화했는데
2887
+ * (`.agentdeck-diag.log` 00:54:59 `enter-label text="지금도 그랬"`), 그 핸들러는
2888
+ * 수식키가 하나라도 눌려 있으면 맨 앞에서 빠져나간다. 즉 그 Enter 는 **shiftKey 가
2889
+ * 지워진 채** 도착했다. 한글 IME 가 조합을 확정하며 Enter 를 한 번 삼키고, 뒤이어
2890
+ * 내보내는 이벤트에는 Shift 상태를 싣지 않는 것이다. Shift 가 없으니 `Shift-Enter`
2891
+ * 핫키에 매칭될 리 없고, xterm 은 평범한 Enter 로 0x0D 를 PTY 에 흘린다 —
2892
+ * 쓰다 만 문장이 그대로 전송된다.
2893
+ *
2894
+ * 그래서 이벤트가 알려주는 수식키를 믿지 않고 **Shift 눌림을 우리가 따로 센다**.
2895
+ * Shift 자체의 keydown/keyup 은 조합과 무관하게 그대로 오므로 IME 를 타지 않는다.
2896
+ * 세어 둔 값이 썩는 것(키업 유실)을 막으려고 창 포커스가 나가거나, Shift 없이 평범한
2897
+ * 글자가 들어오거나, 오래 묵으면(STALE_MS) 스스로 잊는다.
2898
+ *
2899
+ * 조합 중(`isComposing`/keyCode 229)인 Enter 는 손대지 않는다. 그건 "이 음절을
2900
+ * 확정해라" 는 신호고 여기서 막으면 마지막 글자가 확정되지 못한 채 줄만 넘어간다.
2901
+ * 확정이 끝나면 Enter 가 한 번 더 오니 그때 잡으면 글자 순서도 어긋나지 않는다.
2902
+ *
2903
+ * 캡처에서 전파를 끊는 이유는 claimCtrlVKey 와 같다 — 뒤에 걸린 경로(xterm 커스텀 키
2904
+ * 핸들러 → pushKeyEvent, core 의 document 리스너, xterm 이 Enter 를 0x0D 로 흘리는
2905
+ * 기본 동작)를 통째로 죽여야 줄바꿈이 정확히 한 번이 된다.
2906
+ */
2907
+ claimShiftEnterKey() {
2908
+ const STALE_MS = 5000;
2909
+ const forget = () => {
2910
+ this.shiftDownAt = 0;
2911
+ };
2912
+ const isShiftKey = (event) => event.key === 'Shift' || (event.code || '').startsWith('Shift');
2913
+ window.addEventListener('blur', forget);
2914
+ document.addEventListener('keyup', (event) => {
2915
+ if (isShiftKey(event)) {
2916
+ forget();
2917
+ }
2918
+ }, true);
2919
+ document.addEventListener('keydown', (event) => {
2920
+ if (isShiftKey(event)) {
2921
+ this.shiftDownAt = Date.now();
2922
+ return;
2923
+ }
2924
+ if (!this.config.store.agentDeck.claimShiftEnter) {
2925
+ return;
2926
+ }
2927
+ const isEnter = event.key === 'Enter' || event.code === 'Enter' || event.code === 'NumpadEnter';
2928
+ // Shift 없이 평범한 글자가 들어왔다 = Shift 는 확실히 떼어져 있다 (키업 유실 자가복구)
2929
+ if (!isEnter && !event.shiftKey && !event.getModifierState('Shift')) {
2930
+ forget();
2931
+ return;
2932
+ }
2933
+ if (!isEnter || event.altKey || event.metaKey) {
2934
+ return;
2935
+ }
2936
+ if (event.isComposing || event.keyCode === 229) {
2937
+ return; // 조합 확정용 Enter — 확정된 뒤에 한 번 더 온다
2938
+ }
2939
+ const tracked = this.shiftDownAt > 0 && Date.now() - this.shiftDownAt < STALE_MS;
2940
+ const shift = event.shiftKey || event.getModifierState('Shift') || tracked;
2941
+ if (!shift && !event.ctrlKey) {
2942
+ return; // 맨 Enter 는 전송이다 — 건드리지 않는다
2943
+ }
2944
+ if (this.isPlainInput(event.target)) {
2945
+ return;
2946
+ }
2947
+ event.preventDefault();
2948
+ event.stopPropagation();
2949
+ this.diag(`${new Date().toISOString()} newline (capture) shiftKey=${event.shiftKey} `
2950
+ + `modState=${event.getModifierState('Shift')} tracked=${tracked} ctrl=${event.ctrlKey}`);
2951
+ this.sendNewline();
2952
+ }, true);
2953
+ }
2954
+ /**
2955
+ * Enter 를 눌러 프롬프트를 보낼 때, 그 프롬프트 원문을 작업 라벨로 삼는다.
2956
+ *
2957
+ * 왜 이게 필요한가 — 라벨을 채우는 다른 경로(훅·콘솔 제목)는 둘 다 배포판에서 못 믿는다.
2958
+ * 훅은 사용자가 `~/.claude/settings.json` 을 손봐야 돌고, 플러그인이 그걸 대신 깔아줄 수도 없다.
2959
+ * 콘솔 제목은 Claude Code 가 세션 첫 작업을 요약해 한 번 박고 그 뒤로 갱신하지 않는다
2960
+ * (2026-08-28 실측: 작업이 네 번 바뀌는 동안 `◑ sendInput 한 줄 vs 두 줄 원인 파악` 그대로).
2961
+ * 그래서 설정이 하나도 없는 상태에서 라벨을 최신으로 유지할 수 있는 건 이 경로뿐이다.
2962
+ *
2963
+ * **Enter 만 잡는다.** Shift+Enter / Ctrl+Enter 는 줄바꿈이라(agentdeck-newline) 아직 전송이
2964
+ * 아니고, 그때 라벨을 바꾸면 쓰다 만 문장이 박힌다. 수식키가 하나라도 눌려 있으면 그냥 흘린다.
2965
+ * IME 조합 중의 Enter(한글 확정)도 전송이 아니므로 제외한다.
2966
+ *
2967
+ * 관측만 하고 이벤트는 절대 막지 않는다 — 여기서 preventDefault 하면 프롬프트가 안 보내진다.
2968
+ */
2969
+ claimEnterLabel() {
2970
+ document.addEventListener('keydown', (event) => {
2971
+ if (!this.config.store.agentDeck.enterAsLabel) {
2972
+ return;
2973
+ }
2974
+ if (event.shiftKey || event.ctrlKey || event.altKey || event.metaKey) {
2975
+ return; // 줄바꿈이지 전송이 아니다
2976
+ }
2977
+ if (event.key !== 'Enter' && event.code !== 'Enter' && event.code !== 'NumpadEnter') {
2978
+ return;
2329
2979
  }
2330
2980
  if (event.isComposing || event.keyCode === 229) {
2331
2981
  return; // 한글 조합 확정용 Enter
@@ -3314,14 +3964,42 @@ let AgentDeckService = class AgentDeckService {
3314
3964
  if (!this.listEl || !this.enabled || this.editing) {
3315
3965
  return;
3316
3966
  }
3317
- const tabs = this.app.tabs;
3318
- const countEl = this.sidebar.querySelector('.ad-head-count');
3319
- countEl.textContent = String(tabs.length);
3967
+ const statusOf = (tab) => this.status.get(tab).status;
3968
+ // 정렬은 복사본에만 — app.tabs 의 순서(순정 탭바)는 그대로 둔다
3969
+ const tabs = this.config.store.agentDeck.sortByStatus
3970
+ ? (0,_order__WEBPACK_IMPORTED_MODULE_12__.sortTabsByStatus)(this.app.tabs, statusOf)
3971
+ : this.app.tabs;
3972
+ this.renderHeadCount(tabs, statusOf);
3320
3973
  this.listEl.innerHTML = '';
3321
3974
  for (const tab of tabs) {
3322
3975
  this.listEl.appendChild(this.renderTab(tab));
3323
3976
  }
3324
3977
  }
3978
+ /**
3979
+ * 헤더 오른쪽의 상태별 집계 — `⏸ 2 ● 3 ○ 1` 처럼 0 이 아닌 상태만 급한 순서로 놓는다.
3980
+ * 총 개수만 보이던 자리다. 세션을 여럿 띄우면 "몇 개가 나를 기다리나" 가 먼저 보여야 한다.
3981
+ * 동적 텍스트는 textContent 로만 넣는다 (innerHTML 금지).
3982
+ */
3983
+ renderHeadCount(tabs, statusOf) {
3984
+ const countEl = this.sidebar.querySelector('.ad-head-count');
3985
+ if (!countEl) {
3986
+ return;
3987
+ }
3988
+ countEl.innerHTML = '';
3989
+ countEl.title = `탭 ${tabs.length}개`;
3990
+ for (const { status, count } of (0,_order__WEBPACK_IMPORTED_MODULE_12__.countByStatus)(tabs, statusOf)) {
3991
+ const style = _api__WEBPACK_IMPORTED_MODULE_8__.STATUS_STYLES[status];
3992
+ const chip = document.createElement('span');
3993
+ chip.className = 'ad-cnt';
3994
+ chip.style.setProperty('--ad-color', style.color);
3995
+ chip.title = `${style.label} ${count}`;
3996
+ chip.textContent = `${style.icon} ${count}`;
3997
+ countEl.appendChild(chip);
3998
+ }
3999
+ if (!tabs.length) {
4000
+ countEl.textContent = '0';
4001
+ }
4002
+ }
3325
4003
  renderTab(tab) {
3326
4004
  const st = this.status.get(tab);
3327
4005
  const style = _api__WEBPACK_IMPORTED_MODULE_8__.STATUS_STYLES[st.status];
@@ -3364,7 +4042,10 @@ let AgentDeckService = class AgentDeckService {
3364
4042
  labelEl.classList.toggle('empty', !st.label);
3365
4043
  row.querySelector('.ad-elapsed').textContent = elapsed;
3366
4044
  const badge = row.querySelector('.ad-badge');
3367
- badge.textContent = style.icon + ' ' + style.label;
4045
+ // 승인대기 이유가 있으면 뒤에 붙인다 — "⏸ 승인대기 · Bash 권한". 여러 세션을 띄워 둔 사람이
4046
+ // 사이드바만 보고 지금 가서 승인할지 정할 수 있게. 원문은 title 로 마우스를 올리면 보인다
4047
+ badge.textContent = style.icon + ' ' + style.label + (st.reason ? ' · ' + st.reason : '');
4048
+ badge.title = st.reason;
3368
4049
  if (st.pinned) {
3369
4050
  badge.classList.add('pinned');
3370
4051
  }
@@ -3429,7 +4110,7 @@ let AgentDeckService = class AgentDeckService {
3429
4110
  }
3430
4111
  const menu = document.createElement('div');
3431
4112
  menu.className = 'ad-menu';
3432
- const statuses = ['running', 'waiting', 'done', 'error', 'idle'];
4113
+ const statuses = ['running', 'waiting', 'limited', 'done', 'error', 'idle'];
3433
4114
  const items = statuses.map(s => {
3434
4115
  const label = _api__WEBPACK_IMPORTED_MODULE_8__.STATUS_STYLES[s].icon + ' ' + _api__WEBPACK_IMPORTED_MODULE_8__.STATUS_STYLES[s].label;
3435
4116
  const action = () => this.status.setManual(tab, s);
@@ -3618,7 +4299,7 @@ const BUSY_RE = [
3618
4299
  /ctrl\+c to (?:interrupt|cancel|stop)/i,
3619
4300
  /\btokens?\b.{0,20}\besc\b/i,
3620
4301
  ];
3621
- const VALID = ['idle', 'running', 'waiting', 'done', 'error'];
4302
+ const VALID = ['idle', 'running', 'waiting', 'limited', 'done', 'error'];
3622
4303
  /** OSC 가 청크 경계에 걸리는 것을 막기 위한 잔여 버퍼 */
3623
4304
  const tails = new WeakMap();
3624
4305
  /**
@@ -3661,6 +4342,16 @@ function applyOutput(status, root, owner, data, autoDetect) {
3661
4342
  status.setAuto(root, 'waiting');
3662
4343
  return;
3663
4344
  }
4345
+ // 승인대기였던 탭에 작업 중 문구가 다시 보이면 사람이 승인한 것이다 — 진행중으로 올린다.
4346
+ // 훅은 이 순간을 알려주지 않는다: 승인 뒤 첫 훅(PostToolUse)은 도구가 끝난 뒤라, 2분짜리 빌드를
4347
+ // 승인하면 그동안 사이드바가 승인대기에 박혀 있다 (2026-09-07 실측 — "승인했는데 진행중으로 안 돌아온다").
4348
+ // 훅이 고정(pinned)한 탭에도 적용해야 하므로 setAuto 가 아니라 resume 을 쓴다.
4349
+ // 제목 스피너(BUSY_MARKS)는 근거로 삼지 않는다 — 승인 대화상자가 떠 있는 동안에도 제목에 남을 수 있다
4350
+ if (status.get(root).status === 'waiting' && BUSY_RE.some(re => re.test(data))) {
4351
+ status.markBusy(root);
4352
+ status.resume(root);
4353
+ return;
4354
+ }
3664
4355
  // 에이전트가 "지금 일하는 중" 이라고 스스로 밝히는 표시를 찾는다
3665
4356
  if (BUSY_MARKS.some(mark => data.includes(mark)) || BUSY_RE.some(re => re.test(data))) {
3666
4357
  status.markBusy(root);
@@ -4094,6 +4785,9 @@ __webpack_require__.r(__webpack_exports__);
4094
4785
  /* harmony import */ var path__WEBPACK_IMPORTED_MODULE_5___default = /*#__PURE__*/__webpack_require__.n(path__WEBPACK_IMPORTED_MODULE_5__);
4095
4786
  /* harmony import */ var _status_service__WEBPACK_IMPORTED_MODULE_6__ = __webpack_require__(/*! ./status.service */ "./src/status.service.ts");
4096
4787
  /* harmony import */ var _claudeHooks__WEBPACK_IMPORTED_MODULE_7__ = __webpack_require__(/*! ./claudeHooks */ "./src/claudeHooks.ts");
4788
+ /* harmony import */ var _bind__WEBPACK_IMPORTED_MODULE_8__ = __webpack_require__(/*! ./bind */ "./src/bind.ts");
4789
+ /* harmony import */ var _reason__WEBPACK_IMPORTED_MODULE_9__ = __webpack_require__(/*! ./reason */ "./src/reason.ts");
4790
+ /* harmony import */ var _tabenv__WEBPACK_IMPORTED_MODULE_10__ = __webpack_require__(/*! ./tabenv */ "./src/tabenv.ts");
4097
4791
  var __decorate = (undefined && undefined.__decorate) || function (decorators, target, key, desc) {
4098
4792
  var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
4099
4793
  if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
@@ -4111,6 +4805,10 @@ var __metadata = (undefined && undefined.__metadata) || function (k, v) {
4111
4805
 
4112
4806
 
4113
4807
 
4808
+
4809
+
4810
+
4811
+ const DIAG_PATH = path__WEBPACK_IMPORTED_MODULE_5__.join(os__WEBPACK_IMPORTED_MODULE_4__.homedir(), '.agentdeck-diag.log');
4114
4812
  /**
4115
4813
  * 에이전트 훅 -> 사이드바 상태 채널. **기본으로 켜져 있다** (`notifyChannel`).
4116
4814
  *
@@ -4162,6 +4860,25 @@ let WorkNotifyService = class WorkNotifyService {
4162
4860
  this.timer = null;
4163
4861
  /** 폴링을 시작한 시각 — 이보다 오래된 파일은 지난 실행의 잔재다 */
4164
4862
  this.startedAt = 0;
4863
+ /**
4864
+ * 탭(루트) -> 그 안 패널들의 셸 PID. 훅이 보낸 조상 PID 와 맞춰 어느 탭의 보고인지 확정한다.
4865
+ * pty 에 물어보는 비동기 작업이라 표로 들고 있고, 탭이 열릴 때와 2초마다 새로 채운다
4866
+ */
4867
+ this.tabPids = new Map();
4868
+ this.pidRefreshing = false;
4869
+ /**
4870
+ * 탭(루트) -> 그 안 패널 셸에 심은 `AGENTDECK_TAB` 값들(tabenv.ts). 훅이 tabId 로 보내면 여기서 바로
4871
+ * 확정하고, 프로세스 계보(tabPids)는 이 표에 없을 때의 폴백이다. 패널의 `profile.options.env` 를 읽어
4872
+ * 다시 채우므로 재시작으로 복원된 탭(복원 토큰에 profile 이 통째로 들어간다, tabby-local
4873
+ * dist/index.js:639-648)도 지난 실행의 id 를 그대로 되찾는다 — 살아 있는 pty 에 다시 붙든, 복원이
4874
+ * 실패해 같은 options 로 새 셸을 띄우든 셸 환경은 그 값이다
4875
+ */
4876
+ this.tabIds = new Map();
4877
+ /** tabAdded$ 를 이미 구독한 분할 루트 — 루트가 사라지면 함께 사라진다 */
4878
+ this.watchedSplits = new WeakSet();
4879
+ this.pollCount = 0;
4880
+ /** 같은 매칭 실패를 폴링마다 diag 에 찍지 않기 위한 마지막 메시지 */
4881
+ this.lastBindLog = new Map();
4165
4882
  }
4166
4883
  /**
4167
4884
  * 훅이 안 걸려 있으면 기동할 때 한 번 물어본다.
@@ -4226,6 +4943,11 @@ let WorkNotifyService = class WorkNotifyService {
4226
4943
  }
4227
4944
  init() {
4228
4945
  this.app.ready$.subscribe(() => {
4946
+ // 이미 열려 있는 탭 중 셸이 아직 안 뜬 패널이 있으면 지금 심는다 (기동 직후의 첫 탭).
4947
+ // 셸이 이미 뜬 패널은 못 심고 계보 폴백으로 남는다
4948
+ for (const root of [...this.app.tabs]) {
4949
+ this.stampRoot(root);
4950
+ }
4229
4951
  // 훅이 안 걸려 있으면 기동할 때 한 번 물어본다. 통로만 열려 있고 보고자가 없으면
4230
4952
  // 아무 일도 일어나지 않는데, 사용자 눈에는 그냥 "상태가 이상하다" 로만 보인다.
4231
4953
  // 기동 직후는 화면이 정신없으니 조금 뒤에 띄운다
@@ -4238,9 +4960,20 @@ let WorkNotifyService = class WorkNotifyService {
4238
4960
  this.startedAt = Date.now();
4239
4961
  this.listen();
4240
4962
  this.timer = setInterval(() => this.poll(), 400);
4963
+ void this.refreshPids();
4964
+ });
4965
+ this.app.tabOpened$.subscribe(tab => {
4966
+ // 셸은 아직 안 떴다 — `openNewTab` 은 `tabsService.create` 직후 `addTabRaw` 에서 동기로 tabOpened 를
4967
+ // 내고(tabby-core dist/index.js:5029), 셸은 그 뒤 프런트엔드가 준비된 다음에야 뜬다(tabby-local
4968
+ // dist/index.js:623-635). 여기서 profile.options.env 에 심어야 셸이 물려받는다
4969
+ this.stampRoot(tab);
4970
+ // 새 탭의 셸 PID 는 세션이 붙은 뒤에야 나온다 — 조금 뒤에 한 번 더 채운다 (2초 주기 폴링도 있다)
4971
+ setTimeout(() => { void this.refreshPids(); }, 1500);
4241
4972
  });
4242
4973
  // 탭이 닫히면 점유를 풀어야 그 자리를 다음 세션이 쓸 수 있다
4243
4974
  this.app.tabClosed$.subscribe(tab => {
4975
+ this.tabPids.delete(tab);
4976
+ this.tabIds.delete(tab);
4244
4977
  const sid = this.tabSession.get(tab);
4245
4978
  if (sid) {
4246
4979
  this.tabSession.delete(tab);
@@ -4248,6 +4981,212 @@ let WorkNotifyService = class WorkNotifyService {
4248
4981
  }
4249
4982
  });
4250
4983
  }
4984
+ /** 진단 한 줄 — deck.service 와 같은 파일(`~/.agentdeck-diag.log`)에 남긴다 */
4985
+ diag(line) {
4986
+ try {
4987
+ fs__WEBPACK_IMPORTED_MODULE_2__.appendFileSync(DIAG_PATH, `${new Date().toISOString()} ${line}\n`);
4988
+ }
4989
+ catch {
4990
+ // 진단일 뿐이다
4991
+ }
4992
+ }
4993
+ tabName(tab) {
4994
+ const anyTab = tab;
4995
+ return JSON.stringify(String(anyTab.customTitle || anyTab.title || '').slice(0, 40));
4996
+ }
4997
+ /**
4998
+ * 탭별 셸 PID 표를 새로 채운다. 로컬 pty 는 `session.pty.getPID()` 로 셸 PID 를 준다
4999
+ * (tabby-local Session — gracefullyKillProcess 가 쓰는 그 경로). SSH 등 PID 가 없는 탭은 빈 목록.
5000
+ */
5001
+ async refreshPids() {
5002
+ var _a;
5003
+ if (this.pidRefreshing) {
5004
+ return;
5005
+ }
5006
+ this.pidRefreshing = true;
5007
+ try {
5008
+ for (const root of [...this.app.tabs]) {
5009
+ const pids = [];
5010
+ for (const pane of this.panesOf(root)) {
5011
+ const pty = (_a = pane === null || pane === void 0 ? void 0 : pane.session) === null || _a === void 0 ? void 0 : _a.pty;
5012
+ if (!pty || typeof pty.getPID !== 'function') {
5013
+ continue;
5014
+ }
5015
+ try {
5016
+ const pid = await pty.getPID();
5017
+ if (Number.isInteger(pid) && pid > 0) {
5018
+ pids.push(pid);
5019
+ }
5020
+ }
5021
+ catch {
5022
+ // 세션이 닫히는 중이다
5023
+ }
5024
+ }
5025
+ const prev = this.tabPids.get(root);
5026
+ if (!prev || prev.join(',') !== pids.join(',')) {
5027
+ this.tabPids.set(root, pids);
5028
+ this.diag(`pid-map tab=${this.tabName(root)} pids=[${pids.join(',')}]`);
5029
+ }
5030
+ }
5031
+ }
5032
+ finally {
5033
+ this.pidRefreshing = false;
5034
+ }
5035
+ }
5036
+ /** 루트 하나의 터미널 패널들 — 분할 래퍼(`app.tabs` 의 원소는 SplitTabComponent)면 자식들, 아니면 자기 자신 */
5037
+ panesOf(root) {
5038
+ const anyRoot = root;
5039
+ return typeof anyRoot.getAllTabs === 'function' ? anyRoot.getAllTabs() : [root];
5040
+ }
5041
+ isLocalProfile(profile) {
5042
+ return !!profile && profile.type === 'local' && !!profile.options && typeof profile.options === 'object';
5043
+ }
5044
+ /**
5045
+ * 새 루트 탭의 셸에 `AGENTDECK_TAB` 을 심는다 — 지금 있는 패널, 복원 토큰 안의 패널, 나중에 분할로 붙는 패널.
5046
+ *
5047
+ * 심는 자리가 맞는 근거 (tabby-local/dist/index.js, 1.0.231):
5048
+ * - 셸은 `onFrontendReady -> initializeSession` 이 `session.start({ ...this.profile.options, width, height })`
5049
+ * 로 띄운다(623-635). 자식 env 는 `mergeEnv(process.env, {TERM_PROGRAM: 'Tabby', …}, substituteEnv(options.env),
5050
+ * config.store.terminal.environment)` 다(1158-1162). 즉 **spawn 시점의 `pane.profile.options.env`** 가 셸 환경이다.
5051
+ * - `ngOnInit` 의 `this.sessionOptions = this.profile.options`(604-605) 는 같은 객체를 가리키는 참조일 뿐
5052
+ * spawn 에 쓰이지 않는다. profile 을 새 객체로 바꾸므로 그것도 같이 맞춰 둔다.
5053
+ * - `tab.profile` 은 `getNewTabParameters` 가 deep clone 한 것(898)이라 저장된 프로필과 다른 객체다. 그래도
5054
+ * 여기서 다시 얕게 복제해 넣는다 — 다른 경로(복원·복제 토큰)로 들어온 profile 이 공유 객체일 가능성을 막고,
5055
+ * 탭 하나의 id 가 설정 파일의 프로필로 새는 일이 없게 한다.
5056
+ *
5057
+ * 타이밍 (tabby-core/dist/index.js): `openNewTab`(5075) 은 `wrapAndAddTab`(5086) 에서 `splitTab.addTab(tab)` 을
5058
+ * 부르고 — `add()` 는 첫 await(2620 `initialized$`) 앞에서 자식을 `children` 에 넣는다(2618) — 그 다음
5059
+ * `addTabRaw` 가 `tabOpened.next(splitTab)` 를 낸다(5029). 그래서 tabOpened 시점에 `getAllTabs()` 에 자식이
5060
+ * 있고 `session` 은 아직 null 이다.
5061
+ */
5062
+ stampRoot(root) {
5063
+ this.stampPanes(root);
5064
+ const anyRoot = root;
5065
+ // 복제 탭(duplicateTab, 5245-5249)과 재시작 복원 탭(5003-5006)은 이 시점에 자식이 없고 복원 토큰
5066
+ // (`_recoveredState`)만 있다 — 자식은 ngAfterViewInit 의 recoverContainer 가 비동기로 만들고(2488-2491,
5067
+ // 3093 `tabsService.create(recovered)`) tabAdded 도 내지 않는다. 토큰 안의 profile 을 지금 고치면 그것이
5068
+ // 그대로 자식의 profile 이 된다 (recoverTab -> inputs.profile)
5069
+ if (anyRoot._recoveredState && typeof anyRoot._recoveredState === 'object') {
5070
+ this.stampRecoveryState(anyRoot._recoveredState, root);
5071
+ }
5072
+ // 나중에 분할로 붙는 패널 — tabAdded 는 onAfterTabAdded 의 setImmediate 에서 나오는데(2935-2939),
5073
+ // 셸은 그보다 뒤인 프런트엔드 준비 후에 뜬다. 혹시 이미 떠 있으면 stampPane 이 건너뛴다.
5074
+ // Subject 는 루트가 destroy 될 때 complete 되므로(2510) 구독을 따로 풀 필요가 없다
5075
+ if (anyRoot.tabAdded$ && typeof anyRoot.tabAdded$.subscribe === 'function' && !this.watchedSplits.has(root)) {
5076
+ this.watchedSplits.add(root);
5077
+ anyRoot.tabAdded$.subscribe(() => this.stampPanes(root));
5078
+ }
5079
+ }
5080
+ stampPanes(root) {
5081
+ for (const pane of this.panesOf(root)) {
5082
+ this.stampPane(pane, root);
5083
+ }
5084
+ this.refreshTabIds();
5085
+ }
5086
+ /**
5087
+ * 패널 하나에 심는다. **아직 셸이 안 뜬**(`session` 없음) 로컬 패널만 — 이미 뜬 셸의 환경은 바꿀 수 없다.
5088
+ * 복원 패널(`restoreFromPTYID`)은 살아 있는 pty 에 다시 붙으므로 건드리지 않는다 — 토큰의 profile 에
5089
+ * 지난 실행의 id 가 있으면 그게 곧 그 셸의 환경이고 refreshTabIds 가 읽어 간다.
5090
+ * 이미 id 가 있어도 다른 패널이 같은 값을 갖고 있으면(복제) 새로 뽑는다 — 같은 id 가 두 셸에 있으면 어느 쪽인지 알 수 없다.
5091
+ */
5092
+ stampPane(pane, root) {
5093
+ var _a;
5094
+ if (!pane || pane.session || !this.isLocalProfile(pane.profile) || pane.profile.options.restoreFromPTYID) {
5095
+ return;
5096
+ }
5097
+ const env = (_a = pane.profile.options.env) !== null && _a !== void 0 ? _a : {};
5098
+ const cur = typeof env[_tabenv__WEBPACK_IMPORTED_MODULE_10__.TAB_ENV] === 'string' ? env[_tabenv__WEBPACK_IMPORTED_MODULE_10__.TAB_ENV] : '';
5099
+ if (cur && !this.idTakenElsewhere(cur, pane)) {
5100
+ return;
5101
+ }
5102
+ const id = (0,_tabenv__WEBPACK_IMPORTED_MODULE_10__.newTabId)();
5103
+ pane.profile = { ...pane.profile, options: { ...pane.profile.options, env: { ...env, [_tabenv__WEBPACK_IMPORTED_MODULE_10__.TAB_ENV]: id } } };
5104
+ if (pane.sessionOptions !== undefined) {
5105
+ pane.sessionOptions = pane.profile.options;
5106
+ }
5107
+ this.diag(`tabid stamp tab=${this.tabName(root)} id=${id}${cur ? ` (was ${cur}: duplicate)` : ''}`);
5108
+ }
5109
+ /**
5110
+ * 복원 토큰 트리(`app:split-tab` 은 children, 잎은 profile 을 가진다)의 로컬 패널에 심는다.
5111
+ * `restoreFromPTYID` 가 있는 잎은 재시작 복원(saveTabs 가 includeState 로 만든 토큰, tabby-core 7700) —
5112
+ * 살아 있는 pty 의 환경은 이미 정해져 있으니 두고, 없는 잎은 새 셸이 뜰 것이므로(복제 탭 —
5113
+ * `tabsService.duplicate` 는 options 없이 토큰을 만들어 restoreFromPTYID 가 비어 있다, 7819-7830) 항상 새 id 를 준다.
5114
+ * 복제 원본의 id 가 토큰에 그대로 실려 오는데 그걸 두면 두 탭이 같은 id 를 갖게 된다
5115
+ */
5116
+ stampRecoveryState(state, root) {
5117
+ var _a;
5118
+ if (Array.isArray(state.children)) {
5119
+ for (const child of state.children) {
5120
+ if (child && typeof child === 'object') {
5121
+ this.stampRecoveryState(child, root);
5122
+ }
5123
+ }
5124
+ return;
5125
+ }
5126
+ const profile = state.profile;
5127
+ if (!this.isLocalProfile(profile) || profile.options.restoreFromPTYID) {
5128
+ return;
5129
+ }
5130
+ const id = (0,_tabenv__WEBPACK_IMPORTED_MODULE_10__.newTabId)();
5131
+ state.profile = { ...profile, options: { ...profile.options, env: { ...((_a = profile.options.env) !== null && _a !== void 0 ? _a : {}), [_tabenv__WEBPACK_IMPORTED_MODULE_10__.TAB_ENV]: id } } };
5132
+ this.diag(`tabid stamp tab=${this.tabName(root)} id=${id} (recovery token)`);
5133
+ }
5134
+ /** 같은 id 를 다른 패널(어느 루트든)이 이미 갖고 있나 */
5135
+ idTakenElsewhere(id, self) {
5136
+ var _a, _b, _c;
5137
+ for (const root of this.app.tabs) {
5138
+ for (const pane of this.panesOf(root)) {
5139
+ if (pane !== self && ((_c = (_b = (_a = pane === null || pane === void 0 ? void 0 : pane.profile) === null || _a === void 0 ? void 0 : _a.options) === null || _b === void 0 ? void 0 : _b.env) === null || _c === void 0 ? void 0 : _c[_tabenv__WEBPACK_IMPORTED_MODULE_10__.TAB_ENV]) === id) {
5140
+ return true;
5141
+ }
5142
+ }
5143
+ }
5144
+ return false;
5145
+ }
5146
+ /**
5147
+ * 탭별 tabId 표를 패널의 `profile.options.env` 에서 다시 읽는다. 동기·저비용이라 폴링(400ms)마다 돈다 —
5148
+ * 복원 탭의 자식은 tabOpened 뒤에 비동기로 생기고(tabby-core 3093) 이벤트도 없어서 읽는 쪽이 주기적으로 봐야 한다.
5149
+ * 같은 id 가 두 루트에 있으면(복제 뒤 셸이 이미 뜬 경우) 어느 쪽인지 알 수 없으니 둘 다 표에서 빼고 계보 폴백에 맡긴다.
5150
+ */
5151
+ refreshTabIds() {
5152
+ var _a, _b, _c;
5153
+ const owner = new Map();
5154
+ const dup = new Set();
5155
+ const next = new Map();
5156
+ for (const root of [...this.app.tabs]) {
5157
+ const ids = [];
5158
+ for (const pane of this.panesOf(root)) {
5159
+ const v = (_c = (_b = (_a = pane === null || pane === void 0 ? void 0 : pane.profile) === null || _a === void 0 ? void 0 : _a.options) === null || _b === void 0 ? void 0 : _b.env) === null || _c === void 0 ? void 0 : _c[_tabenv__WEBPACK_IMPORTED_MODULE_10__.TAB_ENV];
5160
+ if (typeof v !== 'string' || !v || ids.includes(v)) {
5161
+ continue;
5162
+ }
5163
+ ids.push(v);
5164
+ const prevOwner = owner.get(v);
5165
+ if (prevOwner && prevOwner !== root) {
5166
+ dup.add(v);
5167
+ }
5168
+ else {
5169
+ owner.set(v, root);
5170
+ }
5171
+ }
5172
+ next.set(root, ids);
5173
+ }
5174
+ for (const root of [...this.tabIds.keys()]) {
5175
+ if (!next.has(root)) {
5176
+ this.tabIds.delete(root);
5177
+ }
5178
+ }
5179
+ for (const [root, ids] of next) {
5180
+ const clean = ids.filter(v => !dup.has(v));
5181
+ const prev = this.tabIds.get(root);
5182
+ if (!prev || prev.join(',') !== clean.join(',')) {
5183
+ this.tabIds.set(root, clean);
5184
+ const dropped = ids.filter(v => dup.has(v));
5185
+ this.diag(`tabid-map tab=${this.tabName(root)} ids=[${clean.join(',')}]`
5186
+ + (dropped.length ? ` dup-dropped=[${dropped.join(',')}]` : ''));
5187
+ }
5188
+ }
5189
+ }
4251
5190
  /** 진단용 */
4252
5191
  get debugDir() { return this.dir; }
4253
5192
  /** 진단용 — 듣고 있는 포트 (0 = TCP 채널이 안 떴다) */
@@ -4329,6 +5268,11 @@ let WorkNotifyService = class WorkNotifyService {
4329
5268
  }
4330
5269
  }
4331
5270
  poll() {
5271
+ // tabId 표는 동기라 매번, 셸 PID 표는 pty 에 물어야 해서 2초마다 — 탭 안에서 셸을 다시 띄우거나 분할하면 바뀐다
5272
+ this.refreshTabIds();
5273
+ if (++this.pollCount % 5 === 0) {
5274
+ void this.refreshPids();
5275
+ }
4332
5276
  let names;
4333
5277
  try {
4334
5278
  names = fs__WEBPACK_IMPORTED_MODULE_2__.readdirSync(this.dir);
@@ -4374,6 +5318,7 @@ let WorkNotifyService = class WorkNotifyService {
4374
5318
  * 들고 있는 이쪽이 한다 — 그래야 어느 경로로 들어오든 결과가 같다.
4375
5319
  */
4376
5320
  apply(data) {
5321
+ var _a;
4377
5322
  if (!data || !data.sessionId) {
4378
5323
  return;
4379
5324
  }
@@ -4385,15 +5330,25 @@ let WorkNotifyService = class WorkNotifyService {
4385
5330
  if (this.applied.get(data.sessionId) === data.ts) {
4386
5331
  return;
4387
5332
  }
4388
- const tab = this.resolveTab(data.sessionId);
5333
+ const tab = this.resolveTab(data.sessionId, data.tabId, data.pids);
4389
5334
  if (!tab) {
5335
+ // tabId 도 pids 도 없는 보고(옛 훅·수동 호출)는 다시 봐도 결과가 같다. 그런데 폴링이 400ms 마다
5336
+ // 되풀이하면 "활성 탭에 묶는다" 규칙이 다음에 열리는 빈 탭을 붙잡는다 — 끝난 세션의
5337
+ // done 이 새 탭에 박히고, 그 탭의 진짜 세션은 주인이 있다며 거절됐다 (2026-09-02 실측).
5338
+ // 적용한 것으로 쳐서 멈춘다. 다음 보고(새 ts)가 오면 그때 다시 시도한다.
5339
+ // tabId / pids 가 있는 보고는 표가 채워지면 맞을 수 있으니 다음 폴링에 다시 본다.
5340
+ if (!((_a = data.pids) === null || _a === void 0 ? void 0 : _a.length) && !data.tabId) {
5341
+ this.applied.set(data.sessionId, data.ts);
5342
+ }
4390
5343
  return;
4391
5344
  }
4392
5345
  this.applied.set(data.sessionId, data.ts);
4393
5346
  const label = (data.label || '').trim();
4394
5347
  if (hasStatus) {
4395
5348
  // 라벨이 안 왔으면 undefined 를 넘겨 기존 라벨을 지키게 한다 (setManual 규약)
4396
- this.status.setManual(tab, data.status, hasLabel ? label : undefined);
5349
+ // 이유는 waiting 에서만 의미가 있고 setManual 이 다른 상태에서는 비운다
5350
+ const reason = (0,_reason__WEBPACK_IMPORTED_MODULE_9__.shortenReason)(typeof data.reason === 'string' ? data.reason : '');
5351
+ this.status.setManual(tab, data.status, hasLabel ? label : undefined, reason);
4397
5352
  }
4398
5353
  else {
4399
5354
  // 라벨만 온 보고 — 상태도 고정 여부도 건드리지 않는다
@@ -4406,17 +5361,57 @@ let WorkNotifyService = class WorkNotifyService {
4406
5361
  // 제목과 그 라벨을 각자 줄에 나눠 그린다(renderTab). 여기서 제목을 덮어쓸 이유가 없다.
4407
5362
  }
4408
5363
  /**
4409
- * 세션에 묶인 탭을 찾는다. 처음 보는 세션이면 지금 활성 탭에 묶는다.
5364
+ * 세션에 묶인 탭을 찾는다.
5365
+ *
5366
+ * 1순위는 **셸에 심어 둔 `AGENTDECK_TAB`**(tabId, tabenv.ts) — 플러그인이 탭을 열 때 심은 값을 훅이 그대로
5367
+ * 돌려보낸 것이라 표에 있으면 곧 그 탭이다. 2순위는 **프로세스 계보**(pids) — 훅이 보낸 조상 PID(claude.exe →
5368
+ * 탭 셸 → Tabby) 에 어떤 탭의 셸 PID 가 들어 있으면 그 탭이다(bind.ts). 이 버전 이전에 열린 탭·복원된 탭이
5369
+ * 여기로 온다. 둘 다 이미 다른 세션이 그 탭을 갖고 있어도 넘겨받는다: 같은 탭에서 claude 를 다시 띄우면
5370
+ * 세션 id 가 바뀌는 게 정상이고, 이전 주인은 이미 죽은 세션이다. 알던 탭과 다른 탭이 맞으면 옮긴다
5371
+ * (`claude --resume` 을 다른 탭에서 띄운 경우).
4410
5372
  *
4411
- * 단 그 탭을 이미 다른 세션이 점유했으면 **묶지 않는다**. 예전엔 이 검사가 없어서
4412
- * 여러 세션이 한 탭에 겹쳐 붙었고, 서로의 상태를 덮어써 "작업 중인 탭이 완료로,
4413
- * 끝난 탭이 진행 중으로" 뒤바뀌어 보였다. 못 묶은 세션은 사용자가 그 탭을 활성화하는
4414
- * 순간(= 다음 보고 때) 제 짝을 찾는다.
5373
+ * tabId 나 pids 가 왔는데 맞는 탭이 없으면 **추측하지 않고** null 이다 — 표가 아직 안 채워진 것일 수
5374
+ * 있고(복원 탭의 자식은 늦게 생긴다), 그 경우 다음 폴링(400ms)에 다시 온다. 활성 탭으로 떨어지던 예전 규칙이
5375
+ * 낸 사고가 "끝난 세션의 done 이 새 탭에 박힘" 이었다 (2026-09-02 실측).
5376
+ *
5377
+ * 둘 다 없는 보고(옛 훅·셸에서 사람이 부른 것)만 예전 규칙을 탄다 — 처음 보는 세션이면 지금 활성 탭.
5378
+ * 단 그 탭을 이미 다른 세션이 점유했으면 묶지 않는다 (여러 세션이 한 탭에 겹쳐 붙어 서로의
5379
+ * 상태를 덮어쓰던 2026-08-28 사고의 방지책).
4415
5380
  */
4416
- resolveTab(sessionId) {
5381
+ resolveTab(sessionId, tabId, pids) {
4417
5382
  const known = this.sessionTab.get(sessionId);
4418
- if (known && this.app.tabs.includes(known)) {
4419
- return known;
5383
+ const alive = known && this.app.tabs.includes(known) ? known : null;
5384
+ const byId = (0,_tabenv__WEBPACK_IMPORTED_MODULE_10__.pickTabByTabId)(tabId, this.tabIds);
5385
+ if (byId) {
5386
+ if (byId !== alive) {
5387
+ this.bind(sessionId, byId, alive ? 'tabid-move' : 'tabid');
5388
+ }
5389
+ return byId;
5390
+ }
5391
+ const pick = (0,_bind__WEBPACK_IMPORTED_MODULE_8__.pickTabByPids)(pids, this.tabPids);
5392
+ if (pick.mode === 'pid' && pick.tab) {
5393
+ if (pick.tab !== alive) {
5394
+ this.bind(sessionId, pick.tab, alive ? 'pid-move' : 'pid');
5395
+ }
5396
+ return pick.tab;
5397
+ }
5398
+ if (alive) {
5399
+ return alive;
5400
+ }
5401
+ if (tabId) {
5402
+ // 훅은 tabId 가 있으면 pids 를 보내지 않으므로 여기서 멈춘다 — 계보처럼 추측 없이 다음 폴링을 기다린다
5403
+ const table = [...this.tabIds.entries()]
5404
+ .map(([tab, list]) => `${this.tabName(tab)}:[${list.join(',')}]`)
5405
+ .join(' ');
5406
+ this.bindLog(sessionId, `unbound (tabid-miss) tabId=${tabId} tabs=${table}`);
5407
+ return null;
5408
+ }
5409
+ if (pick.mode === 'pid-miss') {
5410
+ const table = [...this.tabPids.entries()]
5411
+ .map(([tab, list]) => `${this.tabName(tab)}:[${list.join(',')}]`)
5412
+ .join(' ');
5413
+ this.bindLog(sessionId, `unbound (pid-miss) pids=[${(pids !== null && pids !== void 0 ? pids : []).join(',')}] tabs=${table}`);
5414
+ return null;
4420
5415
  }
4421
5416
  const active = this.app.activeTab;
4422
5417
  if (!active) {
@@ -4424,12 +5419,36 @@ let WorkNotifyService = class WorkNotifyService {
4424
5419
  }
4425
5420
  const owner = this.tabSession.get(active);
4426
5421
  if (owner && owner !== sessionId) {
5422
+ this.bindLog(sessionId, `unbound (legacy: active tab owned by ${owner.slice(0, 8)})`);
4427
5423
  return null;
4428
5424
  }
4429
- this.sessionTab.set(sessionId, active);
4430
- this.tabSession.set(active, sessionId);
5425
+ this.bind(sessionId, active, 'legacy');
4431
5426
  return active;
4432
5427
  }
5428
+ /** 세션 <-> 탭 을 묶고, 그 탭의 이전 주인과 이 세션의 이전 탭은 풀어 준다 */
5429
+ bind(sessionId, tab, how) {
5430
+ const prevOwner = this.tabSession.get(tab);
5431
+ if (prevOwner && prevOwner !== sessionId) {
5432
+ this.sessionTab.delete(prevOwner);
5433
+ }
5434
+ const prevTab = this.sessionTab.get(sessionId);
5435
+ if (prevTab && prevTab !== tab && this.tabSession.get(prevTab) === sessionId) {
5436
+ this.tabSession.delete(prevTab);
5437
+ }
5438
+ this.sessionTab.set(sessionId, tab);
5439
+ this.tabSession.set(tab, sessionId);
5440
+ this.lastBindLog.delete(sessionId);
5441
+ this.diag(`bind sid=${sessionId.slice(0, 8)} via=${how} tab=${this.tabName(tab)}`
5442
+ + (prevOwner && prevOwner !== sessionId ? ` (took over from ${prevOwner.slice(0, 8)})` : ''));
5443
+ }
5444
+ /** 매칭 실패는 폴링마다 반복되므로 같은 내용은 한 번만 남긴다 */
5445
+ bindLog(sessionId, msg) {
5446
+ if (this.lastBindLog.get(sessionId) === msg) {
5447
+ return;
5448
+ }
5449
+ this.lastBindLog.set(sessionId, msg);
5450
+ this.diag(`bind sid=${sessionId.slice(0, 8)} ${msg}`);
5451
+ }
4433
5452
  };
4434
5453
  WorkNotifyService = __decorate([
4435
5454
  (0,_angular_core__WEBPACK_IMPORTED_MODULE_0__.Injectable)({ providedIn: 'root' }),
@@ -4441,6 +5460,59 @@ WorkNotifyService = __decorate([
4441
5460
 
4442
5461
 
4443
5462
 
5463
+ /***/ },
5464
+
5465
+ /***/ "./src/order.ts"
5466
+ /*!**********************!*\
5467
+ !*** ./src/order.ts ***!
5468
+ \**********************/
5469
+ (__unused_webpack_module, __webpack_exports__, __webpack_require__) {
5470
+
5471
+ __webpack_require__.r(__webpack_exports__);
5472
+ /* harmony export */ __webpack_require__.d(__webpack_exports__, {
5473
+ /* harmony export */ STATUS_ORDER: () => (/* binding */ STATUS_ORDER),
5474
+ /* harmony export */ countByStatus: () => (/* binding */ countByStatus),
5475
+ /* harmony export */ sortTabsByStatus: () => (/* binding */ sortTabsByStatus),
5476
+ /* harmony export */ statusRank: () => (/* binding */ statusRank)
5477
+ /* harmony export */ });
5478
+ /** 표시·정렬 우선순위. 앞이 더 급하다 */
5479
+ const STATUS_ORDER = ['waiting', 'error', 'limited', 'running', 'done', 'idle'];
5480
+ /** 우선순위 숫자 — 작을수록 앞. 모르는 값은 맨 뒤로 보낸다 */
5481
+ function statusRank(status) {
5482
+ const i = STATUS_ORDER.indexOf(status);
5483
+ return i < 0 ? STATUS_ORDER.length : i;
5484
+ }
5485
+ /**
5486
+ * 상태별 개수. `STATUS_ORDER` 순서로, 0 인 상태는 빼고 돌려준다 — 헤더에 그대로 그리면 된다.
5487
+ */
5488
+ function countByStatus(tabs, statusOf) {
5489
+ var _a, _b;
5490
+ const counts = new Map();
5491
+ for (const tab of tabs) {
5492
+ const s = statusOf(tab);
5493
+ counts.set(s, ((_a = counts.get(s)) !== null && _a !== void 0 ? _a : 0) + 1);
5494
+ }
5495
+ const out = [];
5496
+ for (const s of STATUS_ORDER) {
5497
+ const n = (_b = counts.get(s)) !== null && _b !== void 0 ? _b : 0;
5498
+ if (n > 0) {
5499
+ out.push({ status: s, count: n });
5500
+ }
5501
+ }
5502
+ return out;
5503
+ }
5504
+ /**
5505
+ * 상태 우선순위로 정렬한 **복사본**. 같은 상태끼리는 원래 탭 순서를 지킨다(안정 정렬).
5506
+ * 원본 배열(`app.tabs`)은 건드리지 않는다 — Tabby 의 탭 순서는 사용자의 것이다.
5507
+ */
5508
+ function sortTabsByStatus(tabs, statusOf) {
5509
+ return tabs
5510
+ .map((tab, index) => ({ tab, index, rank: statusRank(statusOf(tab)) }))
5511
+ .sort((a, b) => a.rank - b.rank || a.index - b.index)
5512
+ .map(x => x.tab);
5513
+ }
5514
+
5515
+
4444
5516
  /***/ },
4445
5517
 
4446
5518
  /***/ "./src/profile.service.ts"
@@ -4502,9 +5574,7 @@ let AgentDeckProfileService = class AgentDeckProfileService {
4502
5574
  args: [...(cfg.rootProfileArgs || [])],
4503
5575
  cwd: cfg.rootProfileCwd,
4504
5576
  env: { ...(cfg.rootProfileEnv || {}) },
4505
- // 권한 승격(runAsAdministrator)은 이 플러그인이 다루지 않는다 — Tabby 를 어떤 권한으로
4506
- // 띄울지는 사용자의 몫이다. 단 프로필에 runAsAdministrator 를 켜면 Tabby 가 셸을
4507
- // UAC.exe 릴레이로 감싸고 그 릴레이가 폭 1칸을 잃어 TUI 입력창이 깨진다(README 알려진 문제).
5577
+ // 권한 승격은 이 플러그인이 다루지 않는다 — Tabby 를 어떤 권한으로 띄울지는 사용자의 몫이다.
4508
5578
  },
4509
5579
  };
4510
5580
  if (existing) {
@@ -4646,6 +5716,153 @@ function extractPrompt(lines) {
4646
5716
  }
4647
5717
 
4648
5718
 
5719
+ /***/ },
5720
+
5721
+ /***/ "./src/reason.ts"
5722
+ /*!***********************!*\
5723
+ !*** ./src/reason.ts ***!
5724
+ \***********************/
5725
+ (__unused_webpack_module, __webpack_exports__, __webpack_require__) {
5726
+
5727
+ __webpack_require__.r(__webpack_exports__);
5728
+ /* harmony export */ __webpack_require__.d(__webpack_exports__, {
5729
+ /* harmony export */ shortenReason: () => (/* binding */ shortenReason)
5730
+ /* harmony export */ });
5731
+ /**
5732
+ * 승인대기 이유 줄이기 — Notification 훅의 message 원문을 배지에 붙일 짧은 한국어로 바꾼다.
5733
+ *
5734
+ * 훅(hooks/agentdeck-notify.ps1)은 원문을 그대로 보내고, 줄이는 규칙은 여기 한 곳에만 둔다.
5735
+ * Claude Code 가 문구를 바꾸면 이 파일만 고치면 되고, 훅을 다시 설치할 필요가 없다.
5736
+ *
5737
+ * 알려진 문구 (2026-09 기준):
5738
+ * "Claude needs your permission to use Bash" -> "Bash 권한"
5739
+ * "Claude is waiting for your input" -> "입력 대기"
5740
+ * 플랜 승인 / 질문 응답도 같은 요령으로 잡는다.
5741
+ * 한도 도달(StopFailure error=rate_limit 의 last_assistant_message):
5742
+ * "You've hit your session limit · resets 12pm (Asia/Seoul)" -> "12pm 리셋"
5743
+ * 리셋 시각이 없으면 빈 값 — 배지가 이미 `⛔ 한도 도달` 이라 같은 말을 되풀이하지 않는다.
5744
+ * 모르는 문구는 앞 24자만 남긴다 — 배지 한 줄을 넘기지 않기 위해서다.
5745
+ */
5746
+ /** 모르는 문구를 잘라 낼 길이 */
5747
+ const UNKNOWN_MAX = 24;
5748
+ function shortenReason(msg) {
5749
+ const m = (msg || '').replace(/\s+/g, ' ').trim();
5750
+ if (!m) {
5751
+ return '';
5752
+ }
5753
+ // 도구 승인 — 도구 이름만 남긴다 (Bash, Edit, Write, mcp__vs-mcp__Xxx …). MCP 이름의 '-' 도 포함
5754
+ const tool = /permission to use ([\w-]+)/i.exec(m);
5755
+ if (tool) {
5756
+ return tool[1] + ' 권한';
5757
+ }
5758
+ if (/waiting for your input/i.test(m)) {
5759
+ return '입력 대기';
5760
+ }
5761
+ // 한도 도달 — 리셋 시각만 남긴다. 실측 문구 3형:
5762
+ // "You've hit your session limit · resets 12pm (Asia/Seoul)"
5763
+ // "You've reached your weekly limit · resets Monday 9am"
5764
+ // "Claude usage limit reached. Your limit will reset at 7pm (Asia/Seoul)."
5765
+ if (/\b(?:hit|reached|exceeded)\b.*\blimit\b|usage limit|limit reached|limit will reset/i.test(m)) {
5766
+ const reset = /\breset(?:s)?\s+(?:at\s+)?(.+?)\s*(?:\(|\.\s*$|$)/i.exec(m);
5767
+ return reset ? reset[1].trim() + ' 리셋' : '';
5768
+ }
5769
+ if (/plan/i.test(m)) {
5770
+ return '플랜 승인';
5771
+ }
5772
+ if (/question|answer/i.test(m)) {
5773
+ return '질문 응답';
5774
+ }
5775
+ return m.slice(0, UNKNOWN_MAX).trimEnd();
5776
+ }
5777
+
5778
+
5779
+ /***/ },
5780
+
5781
+ /***/ "./src/recovery.service.ts"
5782
+ /*!*********************************!*\
5783
+ !*** ./src/recovery.service.ts ***!
5784
+ \*********************************/
5785
+ (__unused_webpack_module, __webpack_exports__, __webpack_require__) {
5786
+
5787
+ __webpack_require__.r(__webpack_exports__);
5788
+ /* harmony export */ __webpack_require__.d(__webpack_exports__, {
5789
+ /* harmony export */ AgentDeckRecoveryService: () => (/* binding */ AgentDeckRecoveryService)
5790
+ /* harmony export */ });
5791
+ /* harmony import */ var _angular_core__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! @angular/core */ "@angular/core");
5792
+ /* harmony import */ var _angular_core__WEBPACK_IMPORTED_MODULE_0___default = /*#__PURE__*/__webpack_require__.n(_angular_core__WEBPACK_IMPORTED_MODULE_0__);
5793
+ /* harmony import */ var tabby_core__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(/*! tabby-core */ "tabby-core");
5794
+ /* harmony import */ var tabby_core__WEBPACK_IMPORTED_MODULE_1___default = /*#__PURE__*/__webpack_require__.n(tabby_core__WEBPACK_IMPORTED_MODULE_1__);
5795
+ var __decorate = (undefined && undefined.__decorate) || function (decorators, target, key, desc) {
5796
+ var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
5797
+ if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
5798
+ 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;
5799
+ return c > 3 && r && Object.defineProperty(target, key, r), r;
5800
+ };
5801
+ var __metadata = (undefined && undefined.__metadata) || function (k, v) {
5802
+ if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
5803
+ };
5804
+
5805
+
5806
+ /**
5807
+ * "기동 시 탭 복원 끄기" — Tabby 의 `recoverTabs` 를 강제로 false 에 묶는다.
5808
+ *
5809
+ * 왜 따로 있나: Tabby 는 종료 때 열려 있던 탭을 `localStorage.tabsRecovery` 에 저장하고
5810
+ * 다음 기동에서 `config.store.recoverTabs` 가 참이면 되살린다(tabby-core/dist/index.js:5003).
5811
+ * 그런데 복원되는 것은 **셸만**이다 — 그 안에서 돌던 Claude Code 세션은 이미 죽어 있어
5812
+ * 되살아난 탭은 빈 프롬프트일 뿐이고, 사이드바에는 주인 없는 탭이 줄줄이 남는다.
5813
+ * 그래서 사용자가 고르면 "무조건 빈 상태로 시작" 을 보장한다.
5814
+ *
5815
+ * 두 상태만 있다:
5816
+ * - `agentDeck.noTabRecovery = false` : 순정. Tabby 의 `recoverTabs` 값을 건드리지 않는다.
5817
+ * - `agentDeck.noTabRecovery = true` : 무조건 끔. Tabby 설정 창에서 `recoverTabs` 를 다시 켜도
5818
+ * 기동 때와 설정이 바뀔 때마다 false 로 되돌린다(`enforce`).
5819
+ *
5820
+ * 기동 분기(5003)는 `config.ready$` 직후라 플러그인보다 먼저 지나간다 — 그래서 값은 토글 순간에
5821
+ * config.yaml 에 써 두고, 여기서는 그 뒤로 누가 뒤집어 놓는 경우만 막는다.
5822
+ */
5823
+ let AgentDeckRecoveryService = class AgentDeckRecoveryService {
5824
+ constructor(app, config) {
5825
+ this.app = app;
5826
+ this.config = config;
5827
+ }
5828
+ init() {
5829
+ this.app.ready$.subscribe(() => this.enforce());
5830
+ this.config.changed$.subscribe(() => this.enforce());
5831
+ }
5832
+ /**
5833
+ * 토글 진입점. 켜면 `recoverTabs=false` + 이미 저장된 복원 목록도 지운다 —
5834
+ * 남겨 두면 나중에 순정으로 되돌렸을 때 그때의 낡은 탭들이 되살아난다.
5835
+ * 끄면 Tabby 기본값(true)으로 되돌린다.
5836
+ */
5837
+ setNoTabRecovery(on) {
5838
+ this.config.store.agentDeck.noTabRecovery = on;
5839
+ this.config.store.recoverTabs = !on;
5840
+ if (on) {
5841
+ try {
5842
+ delete window.localStorage.tabsRecovery;
5843
+ }
5844
+ catch { /* 격리/비브라우저 환경 — 없으면 지울 것도 없다 */ }
5845
+ }
5846
+ this.config.save();
5847
+ }
5848
+ /** noTabRecovery 가 켜져 있는데 recoverTabs 가 참으로 돌아와 있으면 다시 끈다. 필요할 때만 저장한다 */
5849
+ enforce() {
5850
+ var _a;
5851
+ if (!((_a = this.config.store.agentDeck) === null || _a === void 0 ? void 0 : _a.noTabRecovery) || this.config.store.recoverTabs === false) {
5852
+ return;
5853
+ }
5854
+ this.config.store.recoverTabs = false;
5855
+ this.config.save();
5856
+ }
5857
+ };
5858
+ AgentDeckRecoveryService = __decorate([
5859
+ (0,_angular_core__WEBPACK_IMPORTED_MODULE_0__.Injectable)({ providedIn: 'root' }),
5860
+ __metadata("design:paramtypes", [tabby_core__WEBPACK_IMPORTED_MODULE_1__.AppService,
5861
+ tabby_core__WEBPACK_IMPORTED_MODULE_1__.ConfigService])
5862
+ ], AgentDeckRecoveryService);
5863
+
5864
+
5865
+
4649
5866
  /***/ },
4650
5867
 
4651
5868
  /***/ "./src/screen.ts"
@@ -4822,6 +6039,7 @@ __webpack_require__.r(__webpack_exports__);
4822
6039
  /* harmony import */ var tabby_core__WEBPACK_IMPORTED_MODULE_1___default = /*#__PURE__*/__webpack_require__.n(tabby_core__WEBPACK_IMPORTED_MODULE_1__);
4823
6040
  /* harmony import */ var _config__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(/*! ./config */ "./src/config.ts");
4824
6041
  /* harmony import */ var _claudeHooks__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(/*! ./claudeHooks */ "./src/claudeHooks.ts");
6042
+ /* harmony import */ var _recovery_service__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(/*! ./recovery.service */ "./src/recovery.service.ts");
4825
6043
  var __decorate = (undefined && undefined.__decorate) || function (decorators, target, key, desc) {
4826
6044
  var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
4827
6045
  if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
@@ -4835,6 +6053,7 @@ var __metadata = (undefined && undefined.__metadata) || function (k, v) {
4835
6053
 
4836
6054
 
4837
6055
 
6056
+
4838
6057
  const DOCK_LABELS = {
4839
6058
  left: '왼쪽',
4840
6059
  right: '오른쪽',
@@ -4844,7 +6063,7 @@ const DOCK_LABELS = {
4844
6063
  /**
4845
6064
  * 설정 > AgentDeck 탭.
4846
6065
  *
4847
- * 사용자가 실제로 바꿀 일이 있는 두 가지만 노출한다 — 레이아웃 on/off 와 작업 루트 프로필.
6066
+ * 사용자가 실제로 바꿀 일이 있는 것만 노출한다 — 레이아웃 on/off, 기동 시 탭 복원 끄기, 작업 루트 프로필.
4848
6067
  * 나머지(화면비, 사이드바 폭, 투명도, 상태 감지, 입력 가로채기, 렌더 보정, 통보 채널)는
4849
6068
  * 손댈 이유가 거의 없고 잘못 만지면 화면이 깨지는 값이라 `config.ts` 의 기본값으로 고정하고
4850
6069
  * 설정 창에서는 뺐다. 정말 필요하면 설정 파일의 `agentDeck.*` 를 직접 고친다.
@@ -4853,8 +6072,9 @@ const DOCK_LABELS = {
4853
6072
  * 루트 프로필은 기동 시 한 번만 읽으므로 재시작이 필요하다고 적어 둔다.
4854
6073
  */
4855
6074
  let AgentDeckSettingsTabComponent = class AgentDeckSettingsTabComponent {
4856
- constructor(config, platform) {
6075
+ constructor(config, recovery, platform) {
4857
6076
  this.config = config;
6077
+ this.recovery = recovery;
4858
6078
  this.platform = platform;
4859
6079
  /** 이 환경에서 훅을 걸 수 있나 (통보 스크립트가 PowerShell 이라 Windows 전용) */
4860
6080
  this.hooksSupported = (0,_claudeHooks__WEBPACK_IMPORTED_MODULE_3__.hooksSupported)();
@@ -4973,6 +6193,18 @@ AgentDeckSettingsTabComponent = __decorate([
4973
6193
  </button>
4974
6194
  </div>
4975
6195
 
6196
+ <div class="form-line">
6197
+ <div class="header">
6198
+ <div class="title">기동 시 탭 복원 끄기</div>
6199
+ <div class="description">
6200
+ 켜면 Tabby 를 다시 열 때 지난 탭을 되살리지 않고 빈 상태로 시작한다 —
6201
+ 복원되는 건 셸뿐이고 그 안의 Claude 세션은 이미 끝나 있어 의미가 없다.
6202
+ 끄면 Tabby 순정 동작(<code>recoverTabs</code>)으로 돌아간다. 다음 기동부터 적용.
6203
+ </div>
6204
+ </div>
6205
+ <toggle [ngModel]="config.store.agentDeck.noTabRecovery" (ngModelChange)="recovery.setNoTabRecovery($event)"></toggle>
6206
+ </div>
6207
+
4976
6208
  <h3 class="mt-4 mb-3">Claude Code 연동</h3>
4977
6209
  <div class="text-muted mb-3">
4978
6210
  에이전트가 "지시를 받았다 / 승인을 기다린다 / 끝났다" 를 직접 알려주게 한다.
@@ -4992,8 +6224,8 @@ AgentDeckSettingsTabComponent = __decorate([
4992
6224
  <div class="title">상태 통보 훅</div>
4993
6225
  <div class="description">
4994
6226
  {{hooksOn ? '설치됨' : '설치 안 됨'}} — <code>~/.claude/settings.json</code> 의
4995
- UserPromptSubmit / Notification / Stop 에 걸린다. 다른 훅은 건드리지 않고,
4996
- 쓰기 전에 같은 폴더에 백업을 남긴다.
6227
+ UserPromptSubmit / Notification / PostToolUse / PostToolUseFailure / PermissionDenied / Stop / StopFailure 에 걸린다.
6228
+ 다른 훅은 건드리지 않고, 쓰기 전에 같은 폴더에 백업을 남긴다.
4997
6229
  <span *ngIf="hookError" class="text-danger d-block mt-1">{{hookError}}</span>
4998
6230
  </div>
4999
6231
  </div>
@@ -5065,6 +6297,7 @@ AgentDeckSettingsTabComponent = __decorate([
5065
6297
  `,
5066
6298
  }),
5067
6299
  __metadata("design:paramtypes", [tabby_core__WEBPACK_IMPORTED_MODULE_1__.ConfigService,
6300
+ _recovery_service__WEBPACK_IMPORTED_MODULE_4__.AgentDeckRecoveryService,
5068
6301
  tabby_core__WEBPACK_IMPORTED_MODULE_1__.PlatformService])
5069
6302
  ], AgentDeckSettingsTabComponent);
5070
6303
 
@@ -5141,6 +6374,11 @@ var __decorate = (undefined && undefined.__decorate) || function (decorators, ta
5141
6374
  };
5142
6375
 
5143
6376
 
6377
+ /**
6378
+ * 부연(reason)을 들고 있을 수 있는 상태. 나머지 상태로 바뀌면 무조건 비운다 —
6379
+ * 끝난 뒤에도 "Bash 권한" 이 남아 있으면 지금 승인을 기다리는 것처럼 읽히기 때문이다
6380
+ */
6381
+ const REASON_STATUSES = ['waiting', 'limited', 'error'];
5144
6382
  /**
5145
6383
  * 탭별 작업 상태 보관소.
5146
6384
  * 상태의 출처는 3가지 — 자동 감지(decorator), OSC 시퀀스(Claude Code 훅), 사이드바에서의 수동 지정.
@@ -5163,7 +6401,7 @@ let WorkStatusService = class WorkStatusService {
5163
6401
  let s = this.states.get(tab);
5164
6402
  if (!s) {
5165
6403
  s = {
5166
- status: 'idle', label: '', since: Date.now(), pinned: false,
6404
+ status: 'idle', label: '', reason: '', since: Date.now(), pinned: false,
5167
6405
  lastOutput: 0, busyMode: false, lastBusy: 0,
5168
6406
  };
5169
6407
  this.states.set(tab, s);
@@ -5178,10 +6416,14 @@ let WorkStatusService = class WorkStatusService {
5178
6416
  }
5179
6417
  s.status = status;
5180
6418
  s.since = Date.now();
6419
+ this.clearReasonUnlessWaiting(s);
5181
6420
  this.changed.next();
5182
6421
  }
5183
- /** 수동/훅 지정 — 이후 자동 감지보다 우선한다 */
5184
- setManual(tab, status, label) {
6422
+ /**
6423
+ * 수동/훅 지정 — 이후 자동 감지보다 우선한다.
6424
+ * `reason` 은 상태의 부연(승인 이유 / 한도 리셋 시각 / 오류 코드) — REASON_STATUSES 일 때만 담는다
6425
+ */
6426
+ setManual(tab, status, label, reason) {
5185
6427
  const s = this.get(tab);
5186
6428
  s.status = status;
5187
6429
  s.since = Date.now();
@@ -5189,6 +6431,23 @@ let WorkStatusService = class WorkStatusService {
5189
6431
  if (label !== undefined) {
5190
6432
  s.label = label;
5191
6433
  }
6434
+ s.reason = REASON_STATUSES.includes(status) ? (reason !== null && reason !== void 0 ? reason : '') : '';
6435
+ this.changed.next();
6436
+ }
6437
+ /**
6438
+ * 승인대기 → 진행중. 사람이 승인한 순간을 훅은 알려주지 않는다 — 다음 훅(PostToolUse)은
6439
+ * 도구가 **끝난 뒤**라 긴 빌드 동안 사이드바가 승인대기에 박혀 있다. 그래서 화면에 "esc to interrupt"
6440
+ * 같은 작업 중 표시가 다시 보이면 여기로 올린다 (detect.ts). pinned·label 은 그대로 둔다 —
6441
+ * 훅이 고정한 탭도 이 전이만은 허용해야 하기 때문이다. waiting 이 아니면 아무것도 하지 않는다
6442
+ */
6443
+ resume(tab) {
6444
+ const s = this.get(tab);
6445
+ if (s.status !== 'waiting') {
6446
+ return;
6447
+ }
6448
+ s.status = 'running';
6449
+ s.since = Date.now();
6450
+ s.reason = '';
5192
6451
  this.changed.next();
5193
6452
  }
5194
6453
  setLabel(tab, label) {
@@ -5201,8 +6460,15 @@ let WorkStatusService = class WorkStatusService {
5201
6460
  s.pinned = false;
5202
6461
  s.status = 'idle';
5203
6462
  s.since = Date.now();
6463
+ this.clearReasonUnlessWaiting(s);
5204
6464
  this.changed.next();
5205
6465
  }
6466
+ /** 부연을 가질 수 없는 상태에는 이유가 남아 있을 수 없다 */
6467
+ clearReasonUnlessWaiting(s) {
6468
+ if (!REASON_STATUSES.includes(s.status)) {
6469
+ s.reason = '';
6470
+ }
6471
+ }
5206
6472
  markOutput(tab) {
5207
6473
  this.get(tab).lastOutput = Date.now();
5208
6474
  }
@@ -5237,6 +6503,7 @@ let WorkStatusService = class WorkStatusService {
5237
6503
  if (from && Date.now() - from > idleAfterMs) {
5238
6504
  s.status = 'idle';
5239
6505
  s.since = Date.now();
6506
+ this.clearReasonUnlessWaiting(s);
5240
6507
  return true;
5241
6508
  }
5242
6509
  return false;
@@ -5248,6 +6515,71 @@ WorkStatusService = __decorate([
5248
6515
 
5249
6516
 
5250
6517
 
6518
+ /***/ },
6519
+
6520
+ /***/ "./src/tabenv.ts"
6521
+ /*!***********************!*\
6522
+ !*** ./src/tabenv.ts ***!
6523
+ \***********************/
6524
+ (__unused_webpack_module, __webpack_exports__, __webpack_require__) {
6525
+
6526
+ __webpack_require__.r(__webpack_exports__);
6527
+ /* harmony export */ __webpack_require__.d(__webpack_exports__, {
6528
+ /* harmony export */ TAB_ENV: () => (/* binding */ TAB_ENV),
6529
+ /* harmony export */ newTabId: () => (/* binding */ newTabId),
6530
+ /* harmony export */ pickTabByTabId: () => (/* binding */ pickTabByTabId)
6531
+ /* harmony export */ });
6532
+ /**
6533
+ * 훅 보고를 탭에 직결하는 열쇠 — 탭 셸에 심는 환경변수와 그것으로 탭을 고르는 순수 규칙.
6534
+ * Angular/Tabby 에 기대지 않아 단위 테스트가 된다 (test/tabenv.test.js).
6535
+ *
6536
+ * 왜 환경변수인가 — 0.2.1 의 프로세스 계보(bind.ts)는 훅이 매 세션 `Win32_Process` 전체를
6537
+ * 훑어(~450ms) 조상 PID 를 보내고 플러그인이 탭별 셸 PID 와 맞추는 방식이다. 맞기는 하지만
6538
+ * 훅이 느리고, 셸 PID 를 pty 에 비동기로 물어야 해서 표가 늦게 채워지는 창이 있다.
6539
+ * 플러그인이 탭을 열 때 셸 환경에 `AGENTDECK_TAB=<id>` 를 심어 두면 그 셸에서 뜬 claude 와
6540
+ * 그 훅은 환경을 그대로 물려받으므로, 훅은 `$env:AGENTDECK_TAB` 을 읽어 보내기만 하면 되고
6541
+ * 플러그인은 표에서 바로 찾는다. 계보 조회가 통째로 빠진다.
6542
+ *
6543
+ * 계보는 폴백으로 남는다 — 이 버전 이전에 열린 탭, 재시작으로 복원된 탭(셸이 이미 떠 있어
6544
+ * 새 값을 심을 수 없다), 사람이 셸 밖에서 부른 훅은 환경변수가 없다.
6545
+ */
6546
+ const TAB_ENV = 'AGENTDECK_TAB';
6547
+ /**
6548
+ * 탭 하나를 가리키는 짧은 무작위 id — 12 hex 자리(48비트). 같은 Tabby 안에서만 유일하면 되므로
6549
+ * 이 길이로 충분하고, 셸 프롬프트나 진단 로그에 찍혀도 거슬리지 않는다.
6550
+ * Electron 렌더러와 Node 20 은 `globalThis.crypto.getRandomValues` 를 둘 다 갖고 있다.
6551
+ */
6552
+ function newTabId() {
6553
+ const bytes = new Uint8Array(6);
6554
+ const c = globalThis.crypto;
6555
+ if (c && typeof c.getRandomValues === 'function') {
6556
+ c.getRandomValues(bytes);
6557
+ }
6558
+ else {
6559
+ for (let i = 0; i < bytes.length; i++) {
6560
+ bytes[i] = Math.floor(Math.random() * 256);
6561
+ }
6562
+ }
6563
+ return Array.from(bytes, b => b.toString(16).padStart(2, '0')).join('');
6564
+ }
6565
+ /**
6566
+ * 훅이 보낸 tabId 를 가진 탭(루트)을 찾는다. 분할 탭은 패널마다 id 가 다르므로 루트 하나에
6567
+ * 여러 id 가 달린다. 없으면 null — 추측하지 않는다 (호출자가 계보/폴백으로 넘어간다).
6568
+ */
6569
+ function pickTabByTabId(tabId, tabIds) {
6570
+ const want = typeof tabId === 'string' ? tabId.trim() : '';
6571
+ if (!want) {
6572
+ return null;
6573
+ }
6574
+ for (const [tab, ids] of tabIds) {
6575
+ if (ids.includes(want)) {
6576
+ return tab;
6577
+ }
6578
+ }
6579
+ return null;
6580
+ }
6581
+
6582
+
5251
6583
  /***/ },
5252
6584
 
5253
6585
  /***/ "net"
@@ -5447,7 +6779,7 @@ let __webpack_exports__ = {};
5447
6779
  \**********************/
5448
6780
  __webpack_require__.r(__webpack_exports__);
5449
6781
  /* harmony export */ __webpack_require__.d(__webpack_exports__, {
5450
- /* harmony export */ STATUS_STYLES: () => (/* reexport safe */ _api__WEBPACK_IMPORTED_MODULE_15__.STATUS_STYLES),
6782
+ /* harmony export */ STATUS_STYLES: () => (/* reexport safe */ _api__WEBPACK_IMPORTED_MODULE_17__.STATUS_STYLES),
5451
6783
  /* harmony export */ "default": () => (__WEBPACK_DEFAULT_EXPORT__)
5452
6784
  /* harmony export */ });
5453
6785
  /* harmony import */ var _angular_core__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! @angular/core */ "@angular/core");
@@ -5468,10 +6800,12 @@ __webpack_require__.r(__webpack_exports__);
5468
6800
  /* harmony import */ var _deck_service__WEBPACK_IMPORTED_MODULE_9__ = __webpack_require__(/*! ./deck.service */ "./src/deck.service.ts");
5469
6801
  /* harmony import */ var _profile_service__WEBPACK_IMPORTED_MODULE_10__ = __webpack_require__(/*! ./profile.service */ "./src/profile.service.ts");
5470
6802
  /* harmony import */ var _notify_service__WEBPACK_IMPORTED_MODULE_11__ = __webpack_require__(/*! ./notify.service */ "./src/notify.service.ts");
5471
- /* harmony import */ var _settings__WEBPACK_IMPORTED_MODULE_12__ = __webpack_require__(/*! ./settings */ "./src/settings.ts");
5472
- /* harmony import */ var _settings_component__WEBPACK_IMPORTED_MODULE_13__ = __webpack_require__(/*! ./settings.component */ "./src/settings.component.ts");
5473
- /* harmony import */ var _styles_scss__WEBPACK_IMPORTED_MODULE_14__ = __webpack_require__(/*! ./styles.scss */ "./src/styles.scss");
5474
- /* harmony import */ var _api__WEBPACK_IMPORTED_MODULE_15__ = __webpack_require__(/*! ./api */ "./src/api.ts");
6803
+ /* harmony import */ var _alert_service__WEBPACK_IMPORTED_MODULE_12__ = __webpack_require__(/*! ./alert.service */ "./src/alert.service.ts");
6804
+ /* harmony import */ var _recovery_service__WEBPACK_IMPORTED_MODULE_13__ = __webpack_require__(/*! ./recovery.service */ "./src/recovery.service.ts");
6805
+ /* harmony import */ var _settings__WEBPACK_IMPORTED_MODULE_14__ = __webpack_require__(/*! ./settings */ "./src/settings.ts");
6806
+ /* harmony import */ var _settings_component__WEBPACK_IMPORTED_MODULE_15__ = __webpack_require__(/*! ./settings.component */ "./src/settings.component.ts");
6807
+ /* harmony import */ var _styles_scss__WEBPACK_IMPORTED_MODULE_16__ = __webpack_require__(/*! ./styles.scss */ "./src/styles.scss");
6808
+ /* harmony import */ var _api__WEBPACK_IMPORTED_MODULE_17__ = __webpack_require__(/*! ./api */ "./src/api.ts");
5475
6809
  var __decorate = (undefined && undefined.__decorate) || function (decorators, target, key, desc) {
5476
6810
  var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
5477
6811
  if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
@@ -5497,12 +6831,17 @@ var __metadata = (undefined && undefined.__metadata) || function (k, v) {
5497
6831
 
5498
6832
 
5499
6833
 
6834
+
6835
+
5500
6836
  let AgentDeckModule = class AgentDeckModule {
5501
- constructor(deck, profiles, notify) {
5502
- // 세 서비스 모두 app.ready$ 를 기다렸다가 스스로 붙는다
6837
+ constructor(deck, profiles, notify, alert, recovery) {
6838
+ // 다섯 서비스 모두 app.ready$ 를 기다렸다가 스스로 붙는다.
6839
+ // alert 는 deck 이 만드는 `__agentdeck` 에 붙으므로 deck 뒤에 둔다
5503
6840
  deck.init();
5504
6841
  profiles.init();
5505
6842
  notify.init();
6843
+ alert.init();
6844
+ recovery.init();
5506
6845
  }
5507
6846
  };
5508
6847
  AgentDeckModule = __decorate([
@@ -5517,15 +6856,17 @@ AgentDeckModule = __decorate([
5517
6856
  { provide: tabby_core__WEBPACK_IMPORTED_MODULE_3__.ConfigProvider, useClass: _config__WEBPACK_IMPORTED_MODULE_6__.AgentDeckConfigProvider, multi: true },
5518
6857
  { provide: tabby_core__WEBPACK_IMPORTED_MODULE_3__.HotkeyProvider, useClass: _hotkeys__WEBPACK_IMPORTED_MODULE_7__.AgentDeckHotkeyProvider, multi: true },
5519
6858
  { provide: tabby_terminal__WEBPACK_IMPORTED_MODULE_5__.TerminalDecorator, useClass: _decorator__WEBPACK_IMPORTED_MODULE_8__.AgentDeckDecorator, multi: true },
5520
- { provide: tabby_settings__WEBPACK_IMPORTED_MODULE_4__.SettingsTabProvider, useClass: _settings__WEBPACK_IMPORTED_MODULE_12__.AgentDeckSettingsTabProvider, multi: true },
6859
+ { provide: tabby_settings__WEBPACK_IMPORTED_MODULE_4__.SettingsTabProvider, useClass: _settings__WEBPACK_IMPORTED_MODULE_14__.AgentDeckSettingsTabProvider, multi: true },
5521
6860
  ],
5522
6861
  declarations: [
5523
- _settings_component__WEBPACK_IMPORTED_MODULE_13__.AgentDeckSettingsTabComponent,
6862
+ _settings_component__WEBPACK_IMPORTED_MODULE_15__.AgentDeckSettingsTabComponent,
5524
6863
  ],
5525
6864
  }),
5526
6865
  __metadata("design:paramtypes", [_deck_service__WEBPACK_IMPORTED_MODULE_9__.AgentDeckService,
5527
6866
  _profile_service__WEBPACK_IMPORTED_MODULE_10__.AgentDeckProfileService,
5528
- _notify_service__WEBPACK_IMPORTED_MODULE_11__.WorkNotifyService])
6867
+ _notify_service__WEBPACK_IMPORTED_MODULE_11__.WorkNotifyService,
6868
+ _alert_service__WEBPACK_IMPORTED_MODULE_12__.AgentDeckAlertService,
6869
+ _recovery_service__WEBPACK_IMPORTED_MODULE_13__.AgentDeckRecoveryService])
5529
6870
  ], AgentDeckModule);
5530
6871
  /* harmony default export */ const __WEBPACK_DEFAULT_EXPORT__ = (AgentDeckModule);
5531
6872