tabby-agentdeck 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +240 -0
- package/dist/api.d.ts +21 -0
- package/dist/config.d.ts +179 -0
- package/dist/deck.service.d.ts +331 -0
- package/dist/decorator.d.ts +28 -0
- package/dist/detect.d.ts +15 -0
- package/dist/hotkeys.d.ts +12 -0
- package/dist/index.d.ts +8 -0
- package/dist/index.js +3535 -0
- package/dist/index.js.map +1 -0
- package/dist/notify.service.d.ts +89 -0
- package/dist/profile.service.d.ts +16 -0
- package/dist/prompt.d.ts +26 -0
- package/dist/status.service.d.ts +32 -0
- package/hooks/agentdeck-label.js +53 -0
- package/hooks/agentdeck-notify.ps1 +159 -0
- package/hooks/agentdeck-status.ps1 +33 -0
- package/install.ps1 +82 -0
- package/package.json +50 -0
|
@@ -0,0 +1,331 @@
|
|
|
1
|
+
import { NgZone } from '@angular/core';
|
|
2
|
+
import { AppService, ConfigService, HostWindowService, HotkeysService, ProfilesService } from 'tabby-core';
|
|
3
|
+
import { WorkStatusService } from './status.service';
|
|
4
|
+
/**
|
|
5
|
+
* AgentDeck 본체.
|
|
6
|
+
*
|
|
7
|
+
* 하는 일 두 가지 —
|
|
8
|
+
* 1) 터미널 뷰포트(.content.main)를 창 높이 기준 4:3 폭으로 고정하고
|
|
9
|
+
* 2) 그때 남는 왼쪽 공간에 탭 목록 + 작업 상태 사이드바를 그린다.
|
|
10
|
+
*
|
|
11
|
+
* Tabby 순정 탭바는 CSS 로 감추고 이 사이드바가 탭 관리를 대신한다.
|
|
12
|
+
* Angular 컴포넌트 대신 순수 DOM 을 쓰는 이유는 appRoot 템플릿에 컴포넌트를
|
|
13
|
+
* 꽂는 공식 확장점이 없기 때문 — DOM 직접 조작이 Tabby 버전 변화에 더 견딘다.
|
|
14
|
+
*/
|
|
15
|
+
export declare class AgentDeckService {
|
|
16
|
+
private app;
|
|
17
|
+
private config;
|
|
18
|
+
private status;
|
|
19
|
+
private hotkeys;
|
|
20
|
+
private profiles;
|
|
21
|
+
private hostWindow;
|
|
22
|
+
private zone;
|
|
23
|
+
private sidebar;
|
|
24
|
+
private listEl;
|
|
25
|
+
private windowEl;
|
|
26
|
+
private mainEl;
|
|
27
|
+
private observer;
|
|
28
|
+
private editing;
|
|
29
|
+
private renderQueued;
|
|
30
|
+
/** rAF 가 멈춘 상황(창 가려짐)을 대비한 렌더 폴백 타이머 */
|
|
31
|
+
private renderTimer;
|
|
32
|
+
/** 직전에 적용한 폭 — 같은 값이면 DOM 을 다시 건드리지 않는다 */
|
|
33
|
+
private lastTermW;
|
|
34
|
+
private lastSidebarW;
|
|
35
|
+
private refitQueued;
|
|
36
|
+
/** 이미 감시를 건 래퍼 탭 / pane — 중복 구독 방지 */
|
|
37
|
+
private watched;
|
|
38
|
+
private watchedPanes;
|
|
39
|
+
/** 첫 화면 복구용 pty 흔들기를 이미 한 pane — 탭당 한 번만 한다 */
|
|
40
|
+
private kickedPanes;
|
|
41
|
+
/** setup() 시각 — 기동 직후 구간인지 판단한다 (0 이면 아직 setup 전) */
|
|
42
|
+
private setupAt;
|
|
43
|
+
/** 기동 직후 구간에 잡힌 pane 들 — 폭이 바뀌면 이들만 다시 흔든다. 구간이 지나면 비운다 */
|
|
44
|
+
private startupPanes;
|
|
45
|
+
/** 진단 기록 — devtools 의 `__agentdeck.kickLog` 와 DIAG_PATH 파일에 함께 남는다 */
|
|
46
|
+
private kickLog;
|
|
47
|
+
/** diagOnce 가 이미 남긴 키 */
|
|
48
|
+
private diagOnceKeys;
|
|
49
|
+
/** 우클릭 누른 시각 — 길게 누르면 컨텍스트 메뉴로 분기 */
|
|
50
|
+
private rightDownAt;
|
|
51
|
+
/** 직전 우클릭 처리 시각 — 같은 클릭이 두 번 들어오는 걸 막는다 */
|
|
52
|
+
private lastRightHandledAt;
|
|
53
|
+
/** xterm 이 네이티브 paste 로 붙여넣은 마지막 시각 — Ctrl+V 이중 입력 방지 */
|
|
54
|
+
private nativePasteAt;
|
|
55
|
+
/** 직전에 붙여넣기를 처리한 시각 — 같은 클릭/키가 여러 경로로 들어와도 한 번만 붙인다 */
|
|
56
|
+
private lastPasteAt;
|
|
57
|
+
/** 이 시각까지는 PTY 로 나가는 입력을 진단 로그에 남긴다 (붙여넣기가 두 번 들어가는지 보려고) */
|
|
58
|
+
private pasteProbeUntil;
|
|
59
|
+
constructor(app: AppService, config: ConfigService, status: WorkStatusService, hotkeys: HotkeysService, profiles: ProfilesService, hostWindow: HostWindowService, zone: NgZone);
|
|
60
|
+
init(): void;
|
|
61
|
+
/**
|
|
62
|
+
* 같은 키로는 한 번만 남기는 진단.
|
|
63
|
+
* relayout 처럼 초당 여러 번 불리는 자리에서 조기 return 사유를 남길 때 쓴다
|
|
64
|
+
*/
|
|
65
|
+
private diagOnce;
|
|
66
|
+
/** 진단 한 줄 — 메모리와 파일에 함께 남긴다. 파일이 안 써져도 동작에는 영향이 없다 */
|
|
67
|
+
private diag;
|
|
68
|
+
private setup;
|
|
69
|
+
/**
|
|
70
|
+
* 탭(그리고 그 안의 모든 pane)의 PTY 출력을 구독한다.
|
|
71
|
+
*
|
|
72
|
+
* TerminalDecorator 는 프론트엔드 첫 리사이즈 시점에만 붙어서 복원된 탭 등을
|
|
73
|
+
* 놓치는 경우가 있다. 이쪽은 탭이 목록에 들어오는 즉시 붙으므로 그 빈틈을 메운다.
|
|
74
|
+
* 같은 출력을 decorator 와 둘 다 먹어도 상태 설정이 멱등이라 결과는 같다.
|
|
75
|
+
*/
|
|
76
|
+
private watchTab;
|
|
77
|
+
private watchPane;
|
|
78
|
+
/**
|
|
79
|
+
* 기동 직후 창을 **복원 -> 최대화**로 한 번 흔든다. 사용자가 손으로 하던 그 동작이다.
|
|
80
|
+
*
|
|
81
|
+
* pty 크기 흔들기(kickPtySize)는 우리가 만든 가짜 SIGWINCH 라, 세션이 늦게 열리거나
|
|
82
|
+
* 폭 변경 타이밍이 어긋나면 놓칠 수 있다. 이쪽은 OS 가 진짜로 창 크기를 두 번 바꾸므로
|
|
83
|
+
* 그 아래 있는 xterm, pty, TUI 가 예외 없이 다시 그린다. 최종 보험이다.
|
|
84
|
+
*
|
|
85
|
+
* 최대화 상태일 때만 한다. 아니면 사용자의 창 상태를 멋대로 최대화해버린다.
|
|
86
|
+
*/
|
|
87
|
+
private scheduleWindowKick;
|
|
88
|
+
/**
|
|
89
|
+
* 탭이 열린 직후 한 번만 pty 크기를 흔들어 TUI 가 화면 전체를 다시 그리게 한다.
|
|
90
|
+
*
|
|
91
|
+
* 왜 필요한가 — 탭이 만들어지는 동안 우리가 `.content.main` 폭을 바꾸면 그 사이에
|
|
92
|
+
* 이미 뜬 TUI(Claude Code 등)는 **옛 폭 기준으로 첫 화면을 그려 놓은 상태**가 된다.
|
|
93
|
+
* 그 뒤 refit/syncPtySize 가 크기를 맞춰도, 맞춘 값이 pty 가 이미 들고 있던 값과 같으면
|
|
94
|
+
* 커널은 SIGWINCH 를 내지 않는다 — TUI 는 "크기 안 바뀌었네" 하고 그대로 있고,
|
|
95
|
+
* 옛 폭으로 그린 입력창 프레임 잔상이 화면에 남는다 (2026-08-28 실측: 첫 한 번만 재현,
|
|
96
|
+
* 창 복원→최대화로 흔들면 즉시 정상 — 그게 SIGWINCH 를 두 번 내기 때문이다).
|
|
97
|
+
*
|
|
98
|
+
* 그래서 같은 일을 코드로 한다: 한 열 좁혔다가 곧바로 되돌린다. 두 번의 SIGWINCH 가
|
|
99
|
+
* 확실히 전달되어 TUI 가 전체 재렌더를 하고 잔상이 지워진다. 탭당 한 번뿐이라
|
|
100
|
+
* 정상 동작하던 탭에는 재렌더 1회 외에 영향이 없다.
|
|
101
|
+
*/
|
|
102
|
+
private scheduleRedrawKick;
|
|
103
|
+
private kickPtySize;
|
|
104
|
+
/**
|
|
105
|
+
* Ctrl+V 를 **핫키 테이블에서 걷어낸다** (실제 처리는 claimCtrlVKey 가 한다).
|
|
106
|
+
*
|
|
107
|
+
* 예전에는 여기서 `agentdeck-paste` 에 Ctrl-V 를 등록했는데, 그러면 한 번 눌러 두 번 붙는
|
|
108
|
+
* 경우가 남았다. Tabby 는 같은 keydown 을 **두 경로**로 HotkeysService 에 밀어 넣기 때문이다 —
|
|
109
|
+
* xterm 의 커스텀 키 핸들러(`tabby-terminal/dist/index.js:40789` keyboardEventHandler →
|
|
110
|
+
* `hotkeysService.pushKeyEvent`)와, document 에 걸린 core 리스너(`tabby-core:21425`).
|
|
111
|
+
* `pushKeyEvent` 앞에 `timeStamp` 중복 가드가 있지만(`tabby-core:21451`) 그건 **같은**
|
|
112
|
+
* 이벤트 객체일 때만 먹고, 두 경로가 서로 다른 이벤트(keydown/keyup 등)로 들어오면
|
|
113
|
+
* `emitHotkeyOn` 이 두 번 나간다(실측: hotkey-paste 가 1ms 간격 2회, `.agentdeck-diag.log`).
|
|
114
|
+
*
|
|
115
|
+
* 그래서 핫키에 기대지 않고 캡처 단계에서 직접 한 번만 처리한다 — 우클릭을 claimRightClick
|
|
116
|
+
* 으로 가져온 것과 같은 이유·같은 방식이다. 순정 `paste` 쪽에 Ctrl-V 가 남아 있어도 캡처가
|
|
117
|
+
* 먼저 먹고 전파를 끊으므로 발화하지 않지만, 설정 화면이 헷갈리지 않게 양쪽 모두에서 뺀다.
|
|
118
|
+
*
|
|
119
|
+
* 대가: 터미널 앱에 리터럴 Ctrl+V(0x16, quoted-insert)를 보낼 수 없다.
|
|
120
|
+
* 그게 필요하면 `agentDeck.claimCtrlV: false`.
|
|
121
|
+
*/
|
|
122
|
+
private ensurePasteHotkey;
|
|
123
|
+
/**
|
|
124
|
+
* Ctrl+V 를 document **캡처 단계**에서 한 번만 처리한다.
|
|
125
|
+
*
|
|
126
|
+
* 캡처는 xterm 의 textarea 리스너보다도 먼저 돌기 때문에, 여기서 전파를 끊으면
|
|
127
|
+
* 그 뒤에 있는 경로(xterm 커스텀 키 핸들러 → pushKeyEvent, core 의 document 리스너,
|
|
128
|
+
* xterm 이 Ctrl+V 를 0x16 으로 바꿔 PTY 에 흘리는 기본 동작, 브라우저 네이티브 paste)가
|
|
129
|
+
* 통째로 죽는다. 몇 개가 겹쳐 걸려 있든 붙여넣기는 정확히 한 번이 된다 —
|
|
130
|
+
* 핫키로 받던 시절 남아 있던 2중 입력의 해법이다(ensurePasteHotkey 주석에 원인).
|
|
131
|
+
*
|
|
132
|
+
* 단, 설정 화면 같은 **평범한 입력 필드**에서는 손대지 않는다. 거기서는 Ctrl+V 가
|
|
133
|
+
* 브라우저 기본 붙여넣기여야 하고, 우리가 가로채면 아예 못 붙여넣게 된다.
|
|
134
|
+
*/
|
|
135
|
+
private claimCtrlVKey;
|
|
136
|
+
/**
|
|
137
|
+
* Enter 를 눌러 프롬프트를 보낼 때, 그 프롬프트 원문을 작업 라벨로 삼는다.
|
|
138
|
+
*
|
|
139
|
+
* 왜 이게 필요한가 — 라벨을 채우는 다른 경로(훅·콘솔 제목)는 둘 다 배포판에서 못 믿는다.
|
|
140
|
+
* 훅은 사용자가 `~/.claude/settings.json` 을 손봐야 돌고, 플러그인이 그걸 대신 깔아줄 수도 없다.
|
|
141
|
+
* 콘솔 제목은 Claude Code 가 세션 첫 작업을 요약해 한 번 박고 그 뒤로 갱신하지 않는다
|
|
142
|
+
* (2026-08-28 실측: 작업이 네 번 바뀌는 동안 `◑ sendInput 한 줄 vs 두 줄 원인 파악` 그대로).
|
|
143
|
+
* 그래서 설정이 하나도 없는 상태에서 라벨을 최신으로 유지할 수 있는 건 이 경로뿐이다.
|
|
144
|
+
*
|
|
145
|
+
* **Enter 만 잡는다.** Shift+Enter / Ctrl+Enter 는 줄바꿈이라(agentdeck-newline) 아직 전송이
|
|
146
|
+
* 아니고, 그때 라벨을 바꾸면 쓰다 만 문장이 박힌다. 수식키가 하나라도 눌려 있으면 그냥 흘린다.
|
|
147
|
+
* IME 조합 중의 Enter(한글 확정)도 전송이 아니므로 제외한다.
|
|
148
|
+
*
|
|
149
|
+
* 관측만 하고 이벤트는 절대 막지 않는다 — 여기서 preventDefault 하면 프롬프트가 안 보내진다.
|
|
150
|
+
*/
|
|
151
|
+
private claimEnterLabel;
|
|
152
|
+
/**
|
|
153
|
+
* xterm 화면 버퍼에서 지금 입력창에 쓰여 있는 프롬프트를 읽는다 (판정은 extractPrompt).
|
|
154
|
+
*
|
|
155
|
+
* 화면 맨 아래가 아니라 **커서 줄**을 기준점으로 잡는다. Claude Code 는 대화가 짧으면
|
|
156
|
+
* 입력창을 화면 위쪽에 두고 그 아래를 전부 빈 줄로 남기는데(2026-08-28 실측), 아래 14줄만
|
|
157
|
+
* 읽으면 빈 줄만 걸려 프롬프트를 영영 못 찾는다 — 라벨이 안 바뀌던 원인이 이것이다.
|
|
158
|
+
* 커서는 항상 입력 중인 줄에 있고, 여러 줄 프롬프트면 마지막 줄에 있으니 거기서 위로 훑으면
|
|
159
|
+
* `> ` 머리를 만난다. 커서 아래는 힌트 줄이라 애초에 읽지 않는 편이 정확하다.
|
|
160
|
+
*/
|
|
161
|
+
private readPromptLine;
|
|
162
|
+
/** 터미널이 아닌 보통 입력 필드(설정 화면 등)인지 — 거기서는 Ctrl+V 를 건드리지 않는다 */
|
|
163
|
+
private isPlainInput;
|
|
164
|
+
/**
|
|
165
|
+
* Ctrl+V 붙여넣기 — 클립보드가 이미지면 Alt+V 로 넘긴다.
|
|
166
|
+
*
|
|
167
|
+
* 터미널 붙여넣기는 "클립보드 텍스트를 키 입력처럼 PTY 에 써넣는" 동작이다
|
|
168
|
+
* (`tabby-terminal/dist/index.js:41842` paste() → readClipboard() → sendInput).
|
|
169
|
+
* PTY 는 바이트 스트림이라 이미지가 지나갈 방법 자체가 없고, 클립보드는 OS 자원이라
|
|
170
|
+
* 터미널 안에서 도는 앱은 손댈 수 없다 — 그래서 이미지는 조용히 사라진다.
|
|
171
|
+
*
|
|
172
|
+
* Claude Code 는 이걸 자기가 OS 클립보드를 직접 읽어서 푼다. Alt+V(= ESC v)가 그
|
|
173
|
+
* "지금 클립보드를 읽어라" 신호다. 즉 Ctrl+V 와 Alt+V 는 붙여넣는 주체가 다르다
|
|
174
|
+
* (Ctrl+V = 터미널이 넣어줌 / Alt+V = 앱이 직접 읽음). 사용자가 그걸 구분할 이유는 없으니
|
|
175
|
+
* 클립보드에 이미지만 있을 때 Ctrl+V 가 알아서 ESC v 를 대신 보낸다.
|
|
176
|
+
*
|
|
177
|
+
* 텍스트가 같이 들어 있으면(엑셀·워드 복사 등) 텍스트 붙여넣기를 유지한다 — 그쪽이 의도이므로.
|
|
178
|
+
*/
|
|
179
|
+
private doPaste;
|
|
180
|
+
/**
|
|
181
|
+
* 이미지 전용 클립보드일 때 터미널 앱에 "네가 클립보드를 직접 읽어라" 키를 흘려보낸다.
|
|
182
|
+
*
|
|
183
|
+
* 어떤 키인지는 앱마다 다르다 (2026-08-28 실측):
|
|
184
|
+
* - Claude Code: ESC v 만 받는다. 리터럴 0x16 은 5회 보내도 이미지가 들어가지 않았다
|
|
185
|
+
* (`.agentdeck-diag.log` 09:33:47~48 `paste image (ctrl-v)` 5줄, 화면에는 아무 변화 없음).
|
|
186
|
+
* claude.exe 안의 `ctrl+v ... paste images` 문구는 자기가 키 이벤트를 직접 볼 때의 안내였고,
|
|
187
|
+
* PTY 로 들어온 0x16 에는 반응하지 않는다.
|
|
188
|
+
* - Codex: 0x16 을 자기 붙여넣기로 받고, ESC v 는 모른다.
|
|
189
|
+
* 그래서 탭에서 도는 앱을 보고 갈라 보내고, 못 알아내면 설정값(`imagePasteKey`)을 쓴다.
|
|
190
|
+
*/
|
|
191
|
+
private sendImagePasteKey;
|
|
192
|
+
/**
|
|
193
|
+
* 이 탭에서 도는 에이전트 CLI 가 뭔지 — 이미지 붙여넣기 키가 앱마다 달라서 필요하다.
|
|
194
|
+
*
|
|
195
|
+
* 프로세스 트리를 먼저 본다 (`session.getChildProcesses()`). npm 전역 래퍼를 거치면
|
|
196
|
+
* 커맨드가 node/pwsh 로만 보일 수 있어서, 그때는 탭 제목으로 한 번 더 본다.
|
|
197
|
+
*/
|
|
198
|
+
private detectAgentApp;
|
|
199
|
+
/**
|
|
200
|
+
* pane.sendInput 을 한 번 감싸 PTY 로 나간 입력을 진단 로그에 남긴다.
|
|
201
|
+
*
|
|
202
|
+
* 붙여넣기 한 번에 텍스트가 두 번 들어가는 증상은 "우리가 두 번 불렀는지" 와
|
|
203
|
+
* "한 번 불렀는데 PTY 에 두 번 써졌는지" 를 갈라야 원인이 잡힌다. 이 계측이 그 경계다.
|
|
204
|
+
* `pasteProbeUntil` 창 안에서만 기록하므로 평소 타이핑은 남지 않는다.
|
|
205
|
+
*/
|
|
206
|
+
private probeSendInput;
|
|
207
|
+
/**
|
|
208
|
+
* PTY 로 나가는 최종 관문(session.write)을 랩핑한다.
|
|
209
|
+
*
|
|
210
|
+
* pane.sendInput 만 보면 그걸 거치지 않는 경로를 놓친다 — 실측(2026-08-28 09:44:03)에서
|
|
211
|
+
* 우클릭 한 번에 sendInput 은 154바이트 1회뿐인데 화면에는 두 번 들어갔다. 즉 두 번째는
|
|
212
|
+
* 우리 계측 밖의 경로다. session.write 는 어느 경로든 마지막에 지나가므로 여기서 잡힌다.
|
|
213
|
+
*/
|
|
214
|
+
private probeWrite;
|
|
215
|
+
/** 호출 지점 몇 프레임 — 두 번째 붙여넣기를 누가 넣는지 보려고 */
|
|
216
|
+
private callSite;
|
|
217
|
+
/** 클립보드에 이미지가 있고 텍스트는 비어 있는지 */
|
|
218
|
+
private clipboardHasImageOnly;
|
|
219
|
+
/** 분할된 탭이면 래퍼가 아니라 실제 포커스된 pane 을 돌려준다 */
|
|
220
|
+
private focusedPane;
|
|
221
|
+
/**
|
|
222
|
+
* 줄바꿈 핫키에 Shift+Enter 를 보정해 넣는다.
|
|
223
|
+
*
|
|
224
|
+
* config.yaml 에 이미 `agentdeck-newline` 이 저장돼 있으면 ConfigProvider 의 defaults 는
|
|
225
|
+
* 무시되므로(저장값이 이긴다) 기동 시 코드로 채워 넣어야 한다 — ensurePasteHotkey 와 같은 이유.
|
|
226
|
+
*/
|
|
227
|
+
private ensureNewlineHotkey;
|
|
228
|
+
/**
|
|
229
|
+
* 우클릭 처리를 agentdeck 이 가져온다.
|
|
230
|
+
*
|
|
231
|
+
* 증상: 우클릭 한 번에 클립보드가 두 번 들어갔다.
|
|
232
|
+
* 원인: Tabby 의 우클릭 처리는 터미널 host 엘리먼트에 건 버블 단계 mouseup 리스너인데
|
|
233
|
+
* (`tabby-terminal/dist/index.js:40972` 에서 등록, `detach()` 는 이 리스너를 떼지 않는다)
|
|
234
|
+
* 프론트엔드가 같은 host 에 다시 attach 되면 리스너가 겹쳐 붙어 mouseEvent$ 가 한 클릭에
|
|
235
|
+
* 두 번 발화한다. 그러면 `handleRightMouseUp` 이 두 번 돌아 paste() 도 두 번 나간다.
|
|
236
|
+
*
|
|
237
|
+
* 해결: Tabby 쪽 처리를 끄고(terminal.rightClick = 'off') document 캡처 단계에서
|
|
238
|
+
* 우리가 한 번만 처리한다. 캡처는 host 리스너보다 먼저 돌기 때문에 몇 개가 겹쳐 붙어 있든
|
|
239
|
+
* stopPropagation 으로 전부 차단된다. 동작 자체는 Tabby 의 'clipboard' 모드와 동일하게 —
|
|
240
|
+
* 선택이 있으면 복사, 없으면 붙여넣기, 길게 누르면 컨텍스트 메뉴 — 유지한다.
|
|
241
|
+
*/
|
|
242
|
+
private claimRightClick;
|
|
243
|
+
/** 사이드바가 아니라 터미널 뷰포트 안에서 일어난 이벤트인지 */
|
|
244
|
+
private inTerminalArea;
|
|
245
|
+
private handleRightClick;
|
|
246
|
+
/**
|
|
247
|
+
* 포커스된 pane 에 0x0A(LF) 를 보낸다 — Ctrl+J 가 보내는 것과 같은 바이트다.
|
|
248
|
+
* Claude Code 는 이걸 줄바꿈으로 받고, Enter(0x0D) 는 전송으로 받는다.
|
|
249
|
+
* Enter 를 눌러 줄을 넘기지 않으려는 것이므로 CR 을 섞으면 안 된다.
|
|
250
|
+
*/
|
|
251
|
+
private sendNewline;
|
|
252
|
+
private get enabled();
|
|
253
|
+
private applyEnabled;
|
|
254
|
+
/**
|
|
255
|
+
* 터미널/사이드바 배경에 알파를 넣어 tabby-background 의 배경 이미지가 비치게 한다.
|
|
256
|
+
*
|
|
257
|
+
* 배경 이미지는 `.content-tab-active::before` 로 **모든 내용 뒤에** 깔린다. 그래서
|
|
258
|
+
* 그 위를 덮는 두 면이 불투명하면 이미지는 어디에도 보이지 않는다 —
|
|
259
|
+
* (1) 터미널: xterm 이 컬러스킴 background 색으로 셀을 직접 칠한다 (CSS 로는 못 뚫는다)
|
|
260
|
+
* (2) 사이드바 / .window: 우리가 넣은 불투명 배경
|
|
261
|
+
* (1) 은 Tabby 가 xterm 을 `allowTransparency: true` 로 만들기 때문에 색에 알파만 넣으면 된다.
|
|
262
|
+
* config.yaml 을 손으로 고쳐도 Tabby 가 종료할 때 메모리 값으로 덮어써 되돌아가므로,
|
|
263
|
+
* ensurePasteHotkey 와 같은 이유로 여기서 코드로 보정한다.
|
|
264
|
+
*/
|
|
265
|
+
private applyOpacity;
|
|
266
|
+
/** 창 높이에서 터미널 폭을 역산해 .content.main 을 고정폭으로 만든다 */
|
|
267
|
+
private relayout;
|
|
268
|
+
/**
|
|
269
|
+
* 폭을 바꿨으면 터미널에 반드시 알려야 한다.
|
|
270
|
+
*
|
|
271
|
+
* Tabby 의 xterm 프론트엔드는 창(OS 창) 리사이즈에 맞춰 재측정하지만,
|
|
272
|
+
* 우리처럼 CSS 로만 `.content.main` 폭을 바꾸면 그 경로를 타지 않는다.
|
|
273
|
+
* 그러면 xterm 의 열 수는 넓어졌는데 pty 는 옛 폭 그대로라, Claude Code 같은
|
|
274
|
+
* 전체화면 TUI 가 화면보다 좁게 그리고 오른쪽에 지워지지 않은 잔상이 남는다
|
|
275
|
+
* (2026-08-28 실측: 입력창 구분선이 화면 중간에서 끊기고 옛 statusline 이 우측에 박혀 있었다.
|
|
276
|
+
* 창을 복원→최대화로 흔들면 OS 리사이즈 경로를 타서 즉시 정상 복구됐다).
|
|
277
|
+
*
|
|
278
|
+
* fitAddon.fit() 이 xterm 의 onResize 를 발화시키고, Tabby 가 그걸 받아
|
|
279
|
+
* session.resize() 로 pty 까지 내려보낸다. 값이 같으면 xterm 이 이벤트를 내지 않으므로
|
|
280
|
+
* 여분 호출은 무해하다.
|
|
281
|
+
*/
|
|
282
|
+
private scheduleRefit;
|
|
283
|
+
private refitTerminals;
|
|
284
|
+
/**
|
|
285
|
+
* pty 크기를 xterm 이 실제로 그리고 있는 크기에 강제로 맞춘다.
|
|
286
|
+
*
|
|
287
|
+
* `fitAddon.fit()` 은 xterm 의 열/행이 이미 맞으면 onResize 를 내지 않는다. 그런데 새 탭은
|
|
288
|
+
* xterm 은 우리가 줄여 놓은 폭으로 제대로 열리는데 **세션(pty)만** 생성 시점의 옛 폭으로 남는
|
|
289
|
+
* 경우가 있다. 이때 fit() 은 no-op 이라 pty 가 영영 안 고쳐지고, TUI 가 처음 화면을 다시
|
|
290
|
+
* 그리는 순간(줄바꿈 등) 오른쪽에 옛 폭 잔상이 남는다 — 창 복원→최대화로 흔들면 OS 리사이즈
|
|
291
|
+
* 경로를 타서 그제야 맞는다(2026-08-28 실측, 새 탭에서 재현).
|
|
292
|
+
*
|
|
293
|
+
* 그래서 fit() 뒤에 pty 로 직접 한 번 더 내려보낸다. 같은 값이면 pty 쪽에서 무시되므로 무해하다.
|
|
294
|
+
*/
|
|
295
|
+
private syncPtySize;
|
|
296
|
+
private buildSidebar;
|
|
297
|
+
/** 설정에 지정된 기본 프로필(기본값 agentdeck:root)로 새 탭을 연다 */
|
|
298
|
+
private openNewTab;
|
|
299
|
+
private openSettings;
|
|
300
|
+
/**
|
|
301
|
+
* 사이드바 다시 그리기를 한 번으로 모은다.
|
|
302
|
+
*
|
|
303
|
+
* rAF 만 쓰면 **창이 가려졌을 때 상태가 늦게 반영된다.** Chromium 은 창이 hidden
|
|
304
|
+
* (최소화·완전 가려짐) 이 되면 rAF 를 아예 멈추고 타이머도 스로틀한다. 그러면
|
|
305
|
+
* 훅이 보낸 done 은 메모리에 즉시 반영됐는데 화면만 옛 상태로 남아 있다가,
|
|
306
|
+
* 1초 tick(그것도 스로틀되면 더 늦게)이 와야 그려진다 — "작업중이 한참 뒤에야 완료로 바뀐다"
|
|
307
|
+
* 의 정체가 이것이다(2026-08-28). 시작 쪽이 빨라 보이는 건 그때는 사용자가 방금
|
|
308
|
+
* 창에 입력해서 창이 떠 있기 때문일 뿐이다.
|
|
309
|
+
*
|
|
310
|
+
* 그래서 rAF 와 짧은 타이머를 같이 걸고 **먼저 오는 쪽이 그린다**. 창이 떠 있으면
|
|
311
|
+
* 지금까지와 똑같이 다음 프레임에, 가려져 있으면 타이머가 대신 그린다.
|
|
312
|
+
*/
|
|
313
|
+
private scheduleRender;
|
|
314
|
+
/**
|
|
315
|
+
* 창이 가려져 있어도 타이머·rAF 가 계속 돌게 한다.
|
|
316
|
+
*
|
|
317
|
+
* 위 scheduleRender 의 타이머 폴백은 "타이머가 제때 돈다" 를 전제로 하는데,
|
|
318
|
+
* Chromium 은 hidden 페이지의 타이머를 1초로 묶고 5분 넘게 가려져 있으면
|
|
319
|
+
* **분당 1회**까지 떨어뜨린다. 그 상태에서는 폴백조차 늦다.
|
|
320
|
+
* Electron 의 backgroundThrottling 을 꺼서 이 계층을 통째로 없앤다.
|
|
321
|
+
*/
|
|
322
|
+
private disableBackgroundThrottling;
|
|
323
|
+
private tick;
|
|
324
|
+
private render;
|
|
325
|
+
private renderTab;
|
|
326
|
+
/** 더블클릭 -> 작업 이름 인라인 편집 */
|
|
327
|
+
private startLabelEdit;
|
|
328
|
+
/** 우클릭 -> 상태 직접 지정 */
|
|
329
|
+
private showStatusMenu;
|
|
330
|
+
private formatElapsed;
|
|
331
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { AppService, ConfigService } from 'tabby-core';
|
|
2
|
+
import { TerminalDecorator, BaseTerminalTabComponent } from 'tabby-terminal';
|
|
3
|
+
import { WorkStatusService } from './status.service';
|
|
4
|
+
/**
|
|
5
|
+
* PTY 출력을 훔쳐보며 탭 상태를 추론한다.
|
|
6
|
+
*
|
|
7
|
+
* Tabby 는 프론트엔드(xterm)가 첫 리사이즈를 보고할 때 decorator 를 붙이므로
|
|
8
|
+
* (tabby-terminal/src/api/baseTerminalTab.component.ts:386) 그 시점을 놓친 탭은
|
|
9
|
+
* 여기로 들어오지 않는다. 그 빈틈은 AgentDeckService 의 세션 워처가 메운다.
|
|
10
|
+
*/
|
|
11
|
+
export declare class AgentDeckDecorator extends TerminalDecorator {
|
|
12
|
+
private status;
|
|
13
|
+
private config;
|
|
14
|
+
private app;
|
|
15
|
+
constructor(status: WorkStatusService, config: ConfigService, app: AppService);
|
|
16
|
+
/**
|
|
17
|
+
* 상태를 기록할 키를 사이드바가 보는 것과 같은 탭으로 맞춘다.
|
|
18
|
+
*
|
|
19
|
+
* Tabby 는 모든 탭을 SplitTabComponent 로 감싸므로 app.tabs 에 들어있는 것은
|
|
20
|
+
* 그 래퍼이고, decorator 가 받는 것은 그 안의 터미널 탭이다. 정규화하지 않으면
|
|
21
|
+
* 상태를 자식에 쓰고 사이드바는 부모를 읽어 영영 매칭되지 않는다.
|
|
22
|
+
* 분할된 pane 여러 개는 하나의 탭 상태를 공유한다.
|
|
23
|
+
*/
|
|
24
|
+
private rootOf;
|
|
25
|
+
attach(tab: BaseTerminalTabComponent<any>): void;
|
|
26
|
+
detach(tab: BaseTerminalTabComponent<any>): void;
|
|
27
|
+
private attachToSession;
|
|
28
|
+
}
|
package/dist/detect.d.ts
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { BaseTabComponent } from 'tabby-core';
|
|
2
|
+
import { WorkStatusService } from './status.service';
|
|
3
|
+
/**
|
|
4
|
+
* PTY 출력 한 조각을 보고 탭 상태를 갱신한다.
|
|
5
|
+
*
|
|
6
|
+
* 우선순위: OSC 통보 > 대기 패턴 > "출력이 있다 = 진행중".
|
|
7
|
+
* idle 로의 복귀는 사이드바 타이머(tickIdle)가 담당한다.
|
|
8
|
+
*
|
|
9
|
+
* decorator 와 deck 의 세션 워처 양쪽에서 호출된다 — 같은 조각을 두 번 먹어도
|
|
10
|
+
* 결과가 같도록 상태 설정은 모두 멱등하게 만들어 두었다.
|
|
11
|
+
*/
|
|
12
|
+
export declare function applyOutput(status: WorkStatusService, root: BaseTabComponent, owner: object, data: string, autoDetect: boolean): void;
|
|
13
|
+
export declare function forgetOutputBuffer(owner: object): void;
|
|
14
|
+
export declare function stripTitleMarker(title: string): string;
|
|
15
|
+
export declare function applyTitle(status: WorkStatusService, root: BaseTabComponent, title: string, autoDetect: boolean): void;
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { HotkeyDescription, HotkeyProvider } from 'tabby-core';
|
|
2
|
+
/**
|
|
3
|
+
* 핫키 "선언"만 한다.
|
|
4
|
+
*
|
|
5
|
+
* 여기서 HotkeysService 를 주입하면 안 된다 — HotkeysService 자신이
|
|
6
|
+
* HotkeyProvider 목록을 주입받으므로(tabby-core/src/services/hotkeys.service.ts:85)
|
|
7
|
+
* 순환 의존이 되어 부분 초기화된 인스턴스가 넘어온다.
|
|
8
|
+
* 실제 핫키 처리는 AgentDeckService 에서 구독한다.
|
|
9
|
+
*/
|
|
10
|
+
export declare class AgentDeckHotkeyProvider extends HotkeyProvider {
|
|
11
|
+
provide(): Promise<HotkeyDescription[]>;
|
|
12
|
+
}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { AgentDeckService } from './deck.service';
|
|
2
|
+
import { AgentDeckProfileService } from './profile.service';
|
|
3
|
+
import { WorkNotifyService } from './notify.service';
|
|
4
|
+
import './styles.scss';
|
|
5
|
+
export * from './api';
|
|
6
|
+
export default class AgentDeckModule {
|
|
7
|
+
constructor(deck: AgentDeckService, profiles: AgentDeckProfileService, notify: WorkNotifyService);
|
|
8
|
+
}
|