tabby-agentdeck 0.3.0 → 1.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +378 -20
- package/dist/agents.d.ts +126 -0
- package/dist/alert.service.d.ts +1 -1
- package/dist/config.d.ts +288 -2
- package/dist/deck.service.d.ts +784 -13
- package/dist/detect.d.ts +22 -3
- package/dist/diag.d.ts +96 -0
- package/dist/gitCommit.d.ts +92 -0
- package/dist/gitDiff.d.ts +99 -0
- package/dist/group.d.ts +181 -0
- package/dist/ime.d.ts +98 -0
- package/dist/index.d.ts +2 -1
- package/dist/index.js +17341 -4644
- package/dist/index.js.map +1 -1
- package/dist/markdown.d.ts +37 -0
- package/dist/nav.d.ts +92 -0
- package/dist/notify.service.d.ts +99 -1
- package/dist/order.d.ts +69 -1
- package/dist/project-root.d.ts +63 -0
- package/dist/prompt.d.ts +35 -1
- package/dist/reason.d.ts +16 -0
- package/dist/screen.d.ts +84 -1
- package/dist/sessionLedger.d.ts +149 -0
- package/dist/sessionLedger.service.d.ts +75 -0
- package/dist/settings.component.d.ts +57 -3
- package/dist/status.service.d.ts +14 -0
- package/dist/subagents.d.ts +120 -0
- package/dist/update.d.ts +76 -0
- package/dist/update.service.d.ts +61 -0
- package/dist/viewPanel.d.ts +384 -0
- package/dist/viewer.d.ts +100 -0
- package/package.json +53 -50
- package/updater/agentdeck-update.ps1 +108 -0
package/dist/deck.service.d.ts
CHANGED
|
@@ -1,6 +1,43 @@
|
|
|
1
1
|
import { NgZone } from '@angular/core';
|
|
2
|
-
import { AppService, ConfigService, HostWindowService, HotkeysService, ProfilesService } from 'tabby-core';
|
|
2
|
+
import { AppService, ConfigService, HostWindowService, HotkeysService, PlatformService, ProfilesService } from 'tabby-core';
|
|
3
3
|
import { WorkStatusService } from './status.service';
|
|
4
|
+
import { WorkNotifyService } from './notify.service';
|
|
5
|
+
import { SessionLedgerService } from './sessionLedger.service';
|
|
6
|
+
/**
|
|
7
|
+
* 검색어와 후보 필드를 비교 가능한 꼴로 접는다.
|
|
8
|
+
*
|
|
9
|
+
* 세 가지를 한꺼번에 처리한다 —
|
|
10
|
+
* 1) 대소문자 무시.
|
|
11
|
+
* 2) 경로 구분자 통일(`\` -> `/`). 후보에는 cwd 원문이 들어오고 Windows 셸은 `\` 를 주는데,
|
|
12
|
+
* 사용자는 `d:\project` 로도 `d:/project` 로도 친다. 접지 않으면 어느 한쪽이 항상 0건이다.
|
|
13
|
+
* 3) 유니코드 정규화(NFC). 한글은 같은 글자를 두 가지로 적을 수 있고(macOS 가 주는 경로는 NFD),
|
|
14
|
+
* Windows IME 가 주는 입력은 NFC 다 — 눈에 같아 보이는 글자가 안 맞는 사고를 막는다.
|
|
15
|
+
*/
|
|
16
|
+
export declare function foldSearchText(raw: string | null | undefined): string;
|
|
17
|
+
/**
|
|
18
|
+
* 검색어를 토큰으로 쪼갠다 — 공백으로 끊고 빈 조각을 버린다.
|
|
19
|
+
*
|
|
20
|
+
* 왜 통째로 안 찾고 쪼개나 — `root api` 처럼 **여러 조각의 AND** 가 사람이 기대하는 동작이다.
|
|
21
|
+
* 폴더가 `Root` 이고 제목에 `api` 가 든 세션을 좁히려면 조각마다 따로 봐야 한다.
|
|
22
|
+
* 통째로 부분문자열을 찾으면 그런 조합은 영원히 0건이 된다.
|
|
23
|
+
*/
|
|
24
|
+
export declare function searchTokens(query: string | null | undefined): string[];
|
|
25
|
+
/**
|
|
26
|
+
* 후보 필드들이 검색 토큰을 전부 만족하나 — 토큰끼리는 AND, 필드끼리는 OR.
|
|
27
|
+
*
|
|
28
|
+
* 토큰이 없으면(검색어가 비었거나 공백뿐) **항상 참**이다. 필터가 없는 상태와 화면이 같아야 한다.
|
|
29
|
+
*/
|
|
30
|
+
export declare function matchesSearchTokens(fields: ReadonlyArray<string | null | undefined>, tokens: readonly string[]): boolean;
|
|
31
|
+
/**
|
|
32
|
+
* 드래그가 끝난 이유. `moved` 만 순서를 바꾼 것이고 나머지는 전부 취소다.
|
|
33
|
+
*
|
|
34
|
+
* `blocked-sort`(상태순 정렬 게이트)와 `no-target`(놓을 자리가 없었다)을 가른 이유 —
|
|
35
|
+
* 둘 다 "안 옮겨졌다" 로 끝나지만 전자는 **막은 것**이고 후자는 **받을 자리가 없었던 것**이다.
|
|
36
|
+
* 회귀가 이 둘을 구별하지 못하면 게이트가 빠져도 초록으로 남는다.
|
|
37
|
+
*/
|
|
38
|
+
export type DragEndReason = 'moved' | 'below-threshold' | 'blocked-sort' | 'esc' | 'cancel' | 'no-target';
|
|
39
|
+
/** 놓을 자리를 거부한 이유 (`dropTargetAt` 이 null 을 준 갈래) */
|
|
40
|
+
export type DropReject = 'outside-list' | 'other-group' | 'self' | 'gone' | 'no-row' | 'stale-index' | null;
|
|
4
41
|
/**
|
|
5
42
|
* AgentDeck 본체.
|
|
6
43
|
*
|
|
@@ -19,6 +56,9 @@ export declare class AgentDeckService {
|
|
|
19
56
|
private hotkeys;
|
|
20
57
|
private profiles;
|
|
21
58
|
private hostWindow;
|
|
59
|
+
private platform;
|
|
60
|
+
private notify;
|
|
61
|
+
private ledger;
|
|
22
62
|
private zone;
|
|
23
63
|
private sidebar;
|
|
24
64
|
private listEl;
|
|
@@ -26,6 +66,64 @@ export declare class AgentDeckService {
|
|
|
26
66
|
private mainEl;
|
|
27
67
|
private observer;
|
|
28
68
|
private editing;
|
|
69
|
+
/**
|
|
70
|
+
* 줄을 끌어 순서를 바꾸는 중일 때의 상태 (null 이면 드래그 중이 아니다).
|
|
71
|
+
*
|
|
72
|
+
* `moved` 가 false 인 구간은 **아직 클릭일 수도 있는** 무장 상태다 — 그 사이에는 화면도
|
|
73
|
+
* 멈추지 않고 삽입선도 그리지 않는다. dock.ts 의 `dragArmed`/`dragging` 와 같은 두 단계다.
|
|
74
|
+
*/
|
|
75
|
+
private drag;
|
|
76
|
+
/**
|
|
77
|
+
* 드래그로 끝난 pointerup 뒤에 따라오는 click 을 한 번 삼킨다.
|
|
78
|
+
*
|
|
79
|
+
* 줄의 click 은 탭 선택이다(`renderTab`). 순서를 옮기고 손을 뗀 것까지 선택으로 읽으면
|
|
80
|
+
* 옮길 때마다 활성 탭이 바뀌어 터미널 화면이 튄다 — 사용자는 순서만 만졌다고 생각한다.
|
|
81
|
+
*
|
|
82
|
+
* **낡은 가드가 남을 수 있다** — 목록 밖에서 손을 떼면 click 이 아예 오지 않으므로 켜 둔
|
|
83
|
+
* 가드를 아무도 안 쓴다. 그러면 그 다음의 정당한 클릭이 삼켜진다. 그래서 소비뿐 아니라
|
|
84
|
+
* **모든 pointerdown 첫 줄에서 끈다**(`armRowDrag`) — click 은 언제나 같은 줄의
|
|
85
|
+
* pointerdown 뒤에 오므로, 이렇게 두면 시간에 기대지 않고도 항상 새 값이 된다.
|
|
86
|
+
*/
|
|
87
|
+
private dragClickGuard;
|
|
88
|
+
/**
|
|
89
|
+
* 마지막 드래그가 어떻게 끝났나 — 회귀가 **의도한 이유로** 취소됐는지 보게 남긴다.
|
|
90
|
+
*
|
|
91
|
+
* 결과(순서 불변)만으로는 부족하다: 낡은 `data-ad-index` 로 대상을 못 찾아 취소된 것과
|
|
92
|
+
* 사용자가 Esc 를 눌러 취소된 것이 결과가 같아서, 판정이 초록인 채로 규칙이 썩는다.
|
|
93
|
+
*/
|
|
94
|
+
private lastDragEnd;
|
|
95
|
+
/** `dropTargetAt` 이 방금 자리를 거부한 이유 (성공이면 null) */
|
|
96
|
+
private dropReject;
|
|
97
|
+
/**
|
|
98
|
+
* 검색 입력창. **`.ad-list` 밖**에 산다 — `render()` 가 목록의 innerHTML 을 비우므로
|
|
99
|
+
* 안에 두면 한 글자 칠 때마다 입력창이 새로 만들어져 포커스와 IME 조합이 날아간다.
|
|
100
|
+
*/
|
|
101
|
+
private searchEl;
|
|
102
|
+
/**
|
|
103
|
+
* 지금 걸린 검색어. **설정에 저장하지 않는다.**
|
|
104
|
+
* 다음 기동에 남아 있으면 목록이 좁혀진 채로 떠서 "탭이 사라졌다" 로 읽힌다 —
|
|
105
|
+
* 이 기능에서 가장 위험한 실패 모양이다. 필터는 지금 이 순간의 도구다.
|
|
106
|
+
*/
|
|
107
|
+
private searchQuery;
|
|
108
|
+
/** 지금 걸린 상태 필터(헤더 칩 클릭). 검색어와 같은 이유로 저장하지 않는다 */
|
|
109
|
+
private statusFilter;
|
|
110
|
+
/** 검색창에서 IME 조합이 도는 중인가 — 조합 중의 Esc 는 조합 취소이므로 검색을 지우면 안 된다 */
|
|
111
|
+
private searchComposing;
|
|
112
|
+
/**
|
|
113
|
+
* 키보드로 짚어 둔 줄 (null 이면 아무 줄도 안 짚었다).
|
|
114
|
+
*
|
|
115
|
+
* **선택(활성 탭)과 다른 값이다.** ↑↓ 는 이 값만 움직이고 `app.selectTab` 은 부르지 않는다 —
|
|
116
|
+
* 이동만으로 탭이 바뀌면 세션을 여섯 개쯤 띄워 둔 사람이 목록을 훑을 수 없다. 줄을 지날 때마다
|
|
117
|
+
* 활성 탭이 바뀌고, 그때마다 터미널이 다시 그려지고(폭 적용·refit) 화면이 튀며, 돌고 있는
|
|
118
|
+
* 탭을 지나가면 그 출력이 화면을 채운다. 그래서 "보고 있는 줄" 과 "쓰고 있는 탭" 을 갈랐고,
|
|
119
|
+
* 둘을 합치는 지점은 Enter 하나다 (`activateNav`).
|
|
120
|
+
*
|
|
121
|
+
* **DOM 이 아니라 여기에 둔다.** `render()` 는 목록을 통째로 다시 만들므로(`innerHTML = ''`)
|
|
122
|
+
* 줄에 얹은 상태는 매 렌더에 날아간다 — 라벨 편집이 `editing` 에, 드래그가 `drag.tab` 에
|
|
123
|
+
* 대상을 객체로 들고 있는 것과 같은 이유다. 인덱스가 아니라 **탭 객체 / 그룹 키**로 들고 있는
|
|
124
|
+
* 근거는 `NavRow` 주석에 있다(인덱스는 탭이 하나 닫히면 다른 줄을 가리킨다).
|
|
125
|
+
*/
|
|
126
|
+
private navFocus;
|
|
29
127
|
private renderQueued;
|
|
30
128
|
/** Shift 물리 키가 눌려 있나 — IME 조합을 거친 Enter 는 shiftKey 가 지워져 온다 */
|
|
31
129
|
private shiftDownAt;
|
|
@@ -39,6 +137,41 @@ export declare class AgentDeckService {
|
|
|
39
137
|
/** 경계선 드래그(크기)와 헤더 드래그(도킹 방향)를 맡는다 */
|
|
40
138
|
private dock;
|
|
41
139
|
private refitQueued;
|
|
140
|
+
/** 결과물 미리보기 패널 (viewPanel.ts) — 사이드바 반대편에 앉는다 */
|
|
141
|
+
private view;
|
|
142
|
+
private lastViewW;
|
|
143
|
+
private lastViewSide;
|
|
144
|
+
/**
|
|
145
|
+
* 탭별 작업 디렉토리 캐시.
|
|
146
|
+
*
|
|
147
|
+
* 미리보기 패널이 화면에서 주운 상대경로(`src/a.ts`)를 풀 때 필요하다. `getWorkingDirectory()`
|
|
148
|
+
* 는 비동기이고 OS 호출이라 출력 조각마다 부를 수 없어서, 필요할 때 한 번 받아 두고
|
|
149
|
+
* `CWD_TTL_MS` 동안 재사용한다 (cd 로 옮겨 다녀도 그 정도면 따라간다).
|
|
150
|
+
*/
|
|
151
|
+
private cwdCache;
|
|
152
|
+
/**
|
|
153
|
+
* 지난 세션 목록에서 "이미 열림" 으로 그린 줄이 가리키는 탭 — `세션id -> 탭`.
|
|
154
|
+
* 매 렌더마다 `liveSessionIds()` 가 다시 채운다(탭이 닫히면 그 줄은 이어받기로 돌아간다).
|
|
155
|
+
*/
|
|
156
|
+
private resumeTabs;
|
|
157
|
+
/**
|
|
158
|
+
* 이 서비스의 출력 구독이 실제로 몇 번 돌았나 (진단용).
|
|
159
|
+
*
|
|
160
|
+
* decorator(`decorator.ts`)도 같은 `output$` 를 구독하므로, 상태 감지가 정상으로 보여도
|
|
161
|
+
* **이쪽 구독은 안 돌고 있을 수 있다.** 그 둘을 가릴 수단이 없어서 검증 중에 막혔다
|
|
162
|
+
* (2026-09-08). `__agentdeck.debug()` 로 읽는다.
|
|
163
|
+
*/
|
|
164
|
+
private outputHits;
|
|
165
|
+
/**
|
|
166
|
+
* 탭별로 판정한 에이전트 (`agents.ts`).
|
|
167
|
+
*
|
|
168
|
+
* 상태 감지 패턴·이미지 붙여넣기 키가 에이전트마다 다르므로, 출력마다 프로세스 트리를
|
|
169
|
+
* 묻지 않으려면 한 번 알아낸 것을 여기 기억해 둬야 한다. 채우는 시점은 세션이 붙을 때와
|
|
170
|
+
* 제목이 바뀔 때 (`probeAgent`, TTL = `AGENT_PROBE_TTL_MS`).
|
|
171
|
+
*/
|
|
172
|
+
private tabAgents;
|
|
173
|
+
/** 마지막으로 프로세스 트리를 물어본 시각 — OS 호출이라 TTL 로 묶는다 */
|
|
174
|
+
private agentProbedAt;
|
|
42
175
|
/** 이미 감시를 건 래퍼 탭 / pane — 중복 구독 방지 */
|
|
43
176
|
private watched;
|
|
44
177
|
private watchedPanes;
|
|
@@ -52,10 +185,8 @@ export declare class AgentDeckService {
|
|
|
52
185
|
private autoRepairAt;
|
|
53
186
|
/** 자동 복구 횟수 — 오탐으로 무한히 흔드는 것을 막는다 */
|
|
54
187
|
private autoRepairCount;
|
|
55
|
-
/**
|
|
56
|
-
private
|
|
57
|
-
/** diagOnce 가 이미 남긴 키 */
|
|
58
|
-
private diagOnceKeys;
|
|
188
|
+
/** 이 탭 화면이 온전해진 시각 — 예산 회복 판정에만 쓴다 (`AUTO_REPAIR_REFILL_MS`) */
|
|
189
|
+
private autoRepairHealthySince;
|
|
59
190
|
/** 우클릭 누른 시각 — 길게 누르면 컨텍스트 메뉴로 분기 */
|
|
60
191
|
private rightDownAt;
|
|
61
192
|
/** 직전 우클릭 처리 시각 — 같은 클릭이 두 번 들어오는 걸 막는다 */
|
|
@@ -66,15 +197,17 @@ export declare class AgentDeckService {
|
|
|
66
197
|
private lastPasteAt;
|
|
67
198
|
/** 이 시각까지는 PTY 로 나가는 입력을 진단 로그에 남긴다 (붙여넣기가 두 번 들어가는지 보려고) */
|
|
68
199
|
private pasteProbeUntil;
|
|
69
|
-
constructor(app: AppService, config: ConfigService, status: WorkStatusService, hotkeys: HotkeysService, profiles: ProfilesService, hostWindow: HostWindowService, zone: NgZone);
|
|
200
|
+
constructor(app: AppService, config: ConfigService, status: WorkStatusService, hotkeys: HotkeysService, profiles: ProfilesService, hostWindow: HostWindowService, platform: PlatformService, notify: WorkNotifyService, ledger: SessionLedgerService, zone: NgZone);
|
|
70
201
|
init(): void;
|
|
71
202
|
/**
|
|
72
203
|
* 같은 키로는 한 번만 남기는 진단.
|
|
73
204
|
* relayout 처럼 초당 여러 번 불리는 자리에서 조기 return 사유를 남길 때 쓴다
|
|
74
205
|
*/
|
|
75
206
|
private diagOnce;
|
|
76
|
-
/** 진단 한 줄 —
|
|
207
|
+
/** 진단 한 줄 — 회전·시각 표기·메모리 보관은 `diag.ts` 가 한다 */
|
|
77
208
|
private diag;
|
|
209
|
+
/** 세션 머리말에 넣을 설정 — 고르는 기준은 `diag.ts` 의 `configStamp` 에 적혀 있다 */
|
|
210
|
+
private diagConfig;
|
|
78
211
|
private setup;
|
|
79
212
|
/**
|
|
80
213
|
* 깨진 화면을 사람이 직접 되살린다 — 창을 복원→최대화로 흔드는 것을 코드로 대신한다.
|
|
@@ -113,10 +246,17 @@ export declare class AgentDeckService {
|
|
|
113
246
|
*/
|
|
114
247
|
private repairPane;
|
|
115
248
|
/**
|
|
116
|
-
*
|
|
249
|
+
* `Ctrl+L`(0x0C) 을 보낸다 — **기본 경로가 아니다** (`repairSendRedrawKey`, 기본 끔).
|
|
117
250
|
*
|
|
118
|
-
*
|
|
119
|
-
*
|
|
251
|
+
* **"거의 모든 TUI 가 이걸 전체 재렌더로 받는다" 는 틀린 전제였다** (2026-09-11 채증으로 확정).
|
|
252
|
+
* Claude Code 는 이걸 **화면 지우기**로 받는다 — 지운 뒤 자기 입력창만 다시 그리므로
|
|
253
|
+
* 보고 있던 대화가 통째로 스크롤백으로 밀려난다. `.agentdeck-screen.log` 06:04:52→54 의
|
|
254
|
+
* `manual-before`(0~77행 전부 내용) → `manual-after`(73~77행만 남음) 가 그 증거이고,
|
|
255
|
+
* `clear input area row=73/78` 이 73행 아래만 지웠음을 진단 로그가 확인해 준다.
|
|
256
|
+
* 즉 0~72행을 날린 것은 우리 지우기가 아니라 이 키다.
|
|
257
|
+
*
|
|
258
|
+
* 그래서 복구의 기본 수단은 이 키가 아니라 **잔상 지우기 + 크기 정합/흔들기**다.
|
|
259
|
+
* 자세한 근거와 켜는 기준은 `config.ts` 의 `repairSendRedrawKey` 주석에 있다.
|
|
120
260
|
*/
|
|
121
261
|
private sendRedrawKey;
|
|
122
262
|
/**
|
|
@@ -152,6 +292,15 @@ export declare class AgentDeckService {
|
|
|
152
292
|
private screenObservingEnabled;
|
|
153
293
|
/** 화면을 판정해 채증하고, 복구가 켜져 있으면 조용히 고친다 */
|
|
154
294
|
private checkScreen;
|
|
295
|
+
/**
|
|
296
|
+
* 화면이 `AUTO_REPAIR_REFILL_MS` 넘게 온전하면 이 탭의 자동복구 예산을 되돌린다.
|
|
297
|
+
*
|
|
298
|
+
* 되돌리는 곳이 아예 없던 것이 결함이었다 — `autoRepairCount` 는 pane 이 살아 있는 동안
|
|
299
|
+
* 단조증가만 해서, 며칠 켜 두는 탭은 `autoRepairMaxPerTab`(기본 3)회를 쓰고 나면 그 뒤의
|
|
300
|
+
* 모든 깨짐을 방치했다. 반대로 온전해지자마자 되돌리면 예산이 사실상 무한이 되므로
|
|
301
|
+
* (깨짐 → 복구 → 온전 → 깨짐 … 이 그대로 반복된다) **오래 온전할 때만** 되돌린다.
|
|
302
|
+
*/
|
|
303
|
+
private refillAutoRepairBudget;
|
|
155
304
|
/** 활성 탭의 pane 들을 한 번씩 본다 (주기 그물) */
|
|
156
305
|
private sweepScreens;
|
|
157
306
|
/** 판정에 넣을 화면 줄과 폭 — 숨겨진 탭이나 아직 안 붙은 프론트엔드는 건너뛴다 */
|
|
@@ -199,7 +348,8 @@ export declare class AgentDeckService {
|
|
|
199
348
|
* 먼저 먹고 전파를 끊으므로 발화하지 않지만, 설정 화면이 헷갈리지 않게 양쪽 모두에서 뺀다.
|
|
200
349
|
*
|
|
201
350
|
* 대가: 터미널 앱에 리터럴 Ctrl+V(0x16, quoted-insert)를 보낼 수 없다.
|
|
202
|
-
* 그게 필요하면 `agentDeck.claimCtrlV: false`.
|
|
351
|
+
* 그게 필요하면 `agentDeck.claimCtrlV: false`. 그때도 **핫키 회수는 그대로 한다** —
|
|
352
|
+
* 이유는 본문 주석(핫키에 남기면 붙여넣기가 먼저 발화해 0x16 이 PTY 에 닿지 못한다).
|
|
203
353
|
*/
|
|
204
354
|
private ensurePasteHotkey;
|
|
205
355
|
/**
|
|
@@ -215,6 +365,29 @@ export declare class AgentDeckService {
|
|
|
215
365
|
* 브라우저 기본 붙여넣기여야 하고, 우리가 가로채면 아예 못 붙여넣게 된다.
|
|
216
366
|
*/
|
|
217
367
|
private claimCtrlVKey;
|
|
368
|
+
/**
|
|
369
|
+
* `Ctrl+W` — 지금 탭을 닫는다. **`keyboardNav` 에 매달려 있다**(`keyboardCloseTab` 주석).
|
|
370
|
+
*
|
|
371
|
+
* 핫키(`hotkeys` 테이블)로 두지 않은 이유가 이 기능의 핵심이다. Tabby 핫키는 **등록되어
|
|
372
|
+
* 있는 것만으로 키를 먹는다** — 우리 처리부에서 설정을 보고 no-op 하더라도 키는 이미
|
|
373
|
+
* 터미널에 닿지 못한다. 그런데 셸·에이전트 CLI 에서 `Ctrl+W` 는 **앞 단어 지우기**(0x17)로
|
|
374
|
+
* 늘 쓰는 키다. 즉 "설정을 껐는데 Ctrl+W 가 죽어 있다" 가 되는데, 그건 이 기능을 끈 사람에게
|
|
375
|
+
* 가장 나쁜 결과다. 캡처 리스너로 두면 **우리가 안 잡을 때 키가 아무 손상 없이 흐른다**.
|
|
376
|
+
* (`claimCtrlVKey` 가 캡처로 간 이유는 달랐다 — 핫키 경로의 2중 발화. 결론만 같다.)
|
|
377
|
+
*
|
|
378
|
+
* 그래서 `preventDefault` 는 **닫을 탭이 실제로 정해졌을 때만** 한다. 고르는 규칙은
|
|
379
|
+
* `nav.ts` 의 `pickCloseTarget` 이고(목록이 키보드를 가졌으면 포커스 줄, 아니면 활성 탭),
|
|
380
|
+
* 그 함수가 `null` 을 주는 경우 — 그룹 헤더 포커스, 이미 닫힌 탭 — 에는 키를 흘린다.
|
|
381
|
+
*
|
|
382
|
+
* 설정 화면 같은 보통 입력 필드(`isPlainInput`)에서도 비켜 준다. 사이드바 안에서 난 키는
|
|
383
|
+
* **목록이 키보드를 가진 때만** 받는다(`isDeckUiKey`) — 검색창은 `isPlainInput` 이 먼저
|
|
384
|
+
* 걸러내고, 나머지(버튼 등)에서 온 `Ctrl+W` 는 우리 것이 아니다.
|
|
385
|
+
*
|
|
386
|
+
* 닫기는 사이드바 `✕` 와 **같은 경로**다 — `closeTab(tab, true)` 이므로 돌아가는 프로세스가
|
|
387
|
+
* 있으면 Tabby 가 평소처럼 확인을 띄운다. 여기서 그 확인을 건너뛰면 키 하나로 세션이
|
|
388
|
+
* 사라진다(에이전트가 일하는 중일 수도 있다).
|
|
389
|
+
*/
|
|
390
|
+
private claimCtrlWKey;
|
|
218
391
|
/**
|
|
219
392
|
* Home / End 를 Tabby 핫키 테이블에서 걷어내 xterm 순정 경로로 돌려준다.
|
|
220
393
|
*
|
|
@@ -238,6 +411,66 @@ export declare class AgentDeckService {
|
|
|
238
411
|
* `agentDeck.releaseHomeEnd: false` 후 설정에서 다시 지정하면 된다.
|
|
239
412
|
*/
|
|
240
413
|
private releaseHomeEndHotkey;
|
|
414
|
+
/**
|
|
415
|
+
* Home / End 를 눌렀을 때 조합 중인 음절을 **먼저 확정**시킨다 (막지는 않는다).
|
|
416
|
+
*
|
|
417
|
+
* 왜 `releaseHomeEndHotkey` 위에 한 겹을 더 얹는가 — 핫키를 놓아주는 것만으로는 부족했다.
|
|
418
|
+
* 실사용 `%APPDATA%\tabby\config.yaml:71-72` 가 이미 `home: []` / `end: []` 인데도 같은
|
|
419
|
+
* 증상이 재발했다 (2026-09-08 사용자 재보고). 즉 그 키를 pty 에 직접 쓰는 경로가 Tabby
|
|
420
|
+
* 핫키 말고도 남아 있다는 뜻이다. 누가 쓰든 우리가 **먼저** 확정을 끝내 두면 순서가 맞는다.
|
|
421
|
+
*
|
|
422
|
+
* 그래서 이 리스너는 캡처 단계에 걸되 **아무것도 막지 않는다**(preventDefault 금지).
|
|
423
|
+
* 캡처는 Tabby 핫키·xterm 키 핸들러보다 먼저 돌기 때문에, 이 시점에 동기 확정을 끝내면
|
|
424
|
+
* 그다음 누가 `\x1b[H` 를 쏘든 음절이 이미 나가 있다.
|
|
425
|
+
*
|
|
426
|
+
* `releaseHomeEndHotkey` 는 그대로 둔다 — 방향키와 같은 경로로 돌려주는 것이 여전히 옳고
|
|
427
|
+
* (xterm 이 확정과 키 전송의 순서를 스스로 지킨다), 이건 그 경로가 다시 선점당했을 때를 위한
|
|
428
|
+
* 두 번째 겹이다. 둘 다 켜져 있어도 확정이 두 번 나가지는 않는다 — 첫 확정 뒤에는
|
|
429
|
+
* `isComposing === false` 라 `readCompositionState` 가 `idle` 로 읽어 아무 일도 하지 않는다.
|
|
430
|
+
*
|
|
431
|
+
* 판정은 이벤트 필드가 아니라 **헬퍼의 살아있는 상태**로 한다. 한글 IME 를 거친 이벤트는
|
|
432
|
+
* `isComposing`·수식키가 지워진 채 도착하는 것이 실측됐다 (`claimShiftEnterKey` 주석).
|
|
433
|
+
*/
|
|
434
|
+
private guardHomeEndComposition;
|
|
435
|
+
/**
|
|
436
|
+
* pty 에 바이트를 쓰는 **공통 진입점** — 조합 중인 음절보다 먼저 나가지 않게 순서를 맞춘다.
|
|
437
|
+
*
|
|
438
|
+
* xterm 을 거치지 않고 `sendInput`/`paste` 로 직접 쓰는 경로는 조합 동기 확정을 트리거하지
|
|
439
|
+
* 않는다 (기전은 `ime.ts` 머리주석에 xterm 원문과 함께). 그래서 쓰기 직전에 조합 상태를 보고
|
|
440
|
+
* - 조합 중이면 동기 확정을 시킨 뒤 쓴다 (방향키가 하는 일을 그대로)
|
|
441
|
+
* - 확정 전송이 예약돼 있으면 우리가 한 틱 뒤로 물러난다 (같은 `setTimeout(…,0)` 큐의 뒤)
|
|
442
|
+
* - 그 외에는 그냥 쓴다
|
|
443
|
+
* 로 갈라 준다. 상태를 못 읽으면(`unknown`) 그냥 쓴다 — 이 훅이 정상 입력을 막는 일은
|
|
444
|
+
* 없어야 한다. `imeOrderGuard: false` 로 끄면 0.4.0 까지의 동작 그대로다.
|
|
445
|
+
*/
|
|
446
|
+
private sendToPane;
|
|
447
|
+
/**
|
|
448
|
+
* 조합이 끝난 **뒤에** 우리 바이트를 쓴다.
|
|
449
|
+
*
|
|
450
|
+
* 왜 확정을 재촉하지 않나 — 우리가 동기 확정시켜도 IME 는 자기 일정대로 `compositionend` 를
|
|
451
|
+
* 또 쏘고, xterm 이 그때 음절을 한 번 더 보낸다. 그 두 번째 전송이 우리 개행 뒤에 나가면
|
|
452
|
+
* **음절이 새 줄로 내려간다**(사용자가 신고한 증상, `ime.ts` planSend 주석에 실측).
|
|
453
|
+
* 그래서 IME 의 생애주기를 건드리지 않고 그것이 끝나기를 기다린다.
|
|
454
|
+
*
|
|
455
|
+
* `compositionend` 가 온 그 순간이 아니라 **그 안에서 예약된 전송이 나간 뒤**에 써야 하므로
|
|
456
|
+
* 한 틱(`setTimeout 0`)을 더 준다 — xterm 의 `_finalizeComposition(true)` 가 같은 큐를 쓴다.
|
|
457
|
+
*
|
|
458
|
+
* 조합이 끝나지 않는 경우(IME 가 이벤트를 안 내거나 창 포커스가 빠짐)를 대비해
|
|
459
|
+
* `IME_WAIT_MS` 뒤에는 동기 확정을 시키고 쓴다 — 입력이 영영 안 나가는 것이 최악이다.
|
|
460
|
+
*/
|
|
461
|
+
private writeAfterComposition;
|
|
462
|
+
/**
|
|
463
|
+
* pane 의 xterm 에서 조합 헬퍼를 집는다.
|
|
464
|
+
*
|
|
465
|
+
* `_core._compositionHelper` 는 xterm 내부다(`tabby-terminal/dist/index.js:50042` 에서 생성).
|
|
466
|
+
* 언제 사라져도 이상하지 않으므로 못 찾으면 null 을 주고, 판정은 `unknown`(= 기존 동작)으로
|
|
467
|
+
* 떨어진다. 예외를 밖으로 내보내면 입력 자체가 막히므로 절대 던지지 않는다.
|
|
468
|
+
*/
|
|
469
|
+
private compositionHelper;
|
|
470
|
+
/** 동기 확정이 읽을 textarea 길이 — 낡은 `_compositionPosition.end` 를 메우는 값 (ime.ts 참조) */
|
|
471
|
+
private compositionTextLength;
|
|
472
|
+
/** 진단 한 줄에 넣을 헬퍼 원본값 — 판정(state)만 남기면 오판을 가릴 수 없다 */
|
|
473
|
+
private describeComposition;
|
|
241
474
|
/**
|
|
242
475
|
* Shift+Enter / Ctrl+Enter 를 document **캡처 단계**에서 직접 줄바꿈으로 바꾼다.
|
|
243
476
|
*
|
|
@@ -293,6 +526,20 @@ export declare class AgentDeckService {
|
|
|
293
526
|
private readPromptLine;
|
|
294
527
|
/** 터미널이 아닌 보통 입력 필드(설정 화면 등)인지 — 거기서는 Ctrl+V 를 건드리지 않는다 */
|
|
295
528
|
private isPlainInput;
|
|
529
|
+
/**
|
|
530
|
+
* 이 키가 **우리 사이드바 안에서** 났나 — 그렇다면 터미널 몫이 아니다.
|
|
531
|
+
*
|
|
532
|
+
* `isPlainInput` 과 나란히 쓰는 두 번째 가드다. 그 함수는 "보통 입력 필드냐" 를 보므로
|
|
533
|
+
* 검색창(`<input>`)은 이미 걸러 주지만, 키보드 내비게이션은 목록(`.ad-list`, DIV)이
|
|
534
|
+
* 포커스를 갖는다 — 태그로는 터미널과 구별되지 않는다. 그대로 두면 목록에서 누른 Enter 가
|
|
535
|
+
* 캡처 단계의 `claimEnterLabel` 에 먼저 걸려 **엉뚱한 탭의 화면을 읽어 라벨로 박고**,
|
|
536
|
+
* Shift+Enter 는 `claimShiftEnterKey` 가 0x0A 를 pty 로 보낸다. 우리 리스너는 target
|
|
537
|
+
* 단계라 캡처보다 늦게 돌아 끊을 수 없으니, 비켜 주는 쪽이 캡처여야 한다.
|
|
538
|
+
*
|
|
539
|
+
* 기존 동작은 바뀌지 않는다 — 이 판정이 참이 되는 상황은 **사이드바가 포커스를 가진 때**뿐이고,
|
|
540
|
+
* 그런 상태는 이 기능이 생기기 전에는 검색창(이미 `isPlainInput` 이 비켜 줬다) 외에 없었다.
|
|
541
|
+
*/
|
|
542
|
+
private isDeckUiKey;
|
|
296
543
|
/**
|
|
297
544
|
* Ctrl+V 붙여넣기 — 클립보드가 이미지면 Alt+V 로 넘긴다.
|
|
298
545
|
*
|
|
@@ -322,12 +569,84 @@ export declare class AgentDeckService {
|
|
|
322
569
|
*/
|
|
323
570
|
private sendImagePasteKey;
|
|
324
571
|
/**
|
|
325
|
-
*
|
|
572
|
+
* 터미널 위에 **파일을 끌어다 놓았을 때** 무엇을 할지 물어본다.
|
|
573
|
+
*
|
|
574
|
+
* 순정 Tabby 는 그 경로를 터미널에 붙여넣는다(`cat <경로>` 를 쉽게 만드는 유용한 동작이다).
|
|
575
|
+
* 그런데 미리보기 패널이 생긴 뒤로는 "그 파일을 보고 싶어서" 끌어놓는 경우가 더 많은데,
|
|
576
|
+
* 패널이 닫혀 있으면 경로만 붙고 아무것도 안 열린다(2026-09-09 유저 지적).
|
|
577
|
+
*
|
|
578
|
+
* 그래서 캡처 단계에서 드롭을 가로채고 **둘 중 하나를 고르게** 한다 —
|
|
579
|
+
* `패널에 띄우기` / `경로 붙여넣기`. 자동으로 화면을 바꾸지 않는 이유는 붙여넣기만 원했던
|
|
580
|
+
* 사람의 화면을 빼앗지 않기 위해서다(유저: "한번 물어보긴 해야 할 듯").
|
|
581
|
+
*
|
|
582
|
+
* `viewerDropOpen` 이 `always`/`paste` 면 묻지 않고 그 선택을 하고, `never` 면 아예
|
|
583
|
+
* 개입하지 않아 순정 동작이 그대로 돈다.
|
|
584
|
+
*/
|
|
585
|
+
private claimFileDrop;
|
|
586
|
+
/** 끌어다 놓은 파일을 미리보기 패널에서 연다 (패널이 닫혀 있으면 열면서) */
|
|
587
|
+
private openDroppedInPanel;
|
|
588
|
+
/**
|
|
589
|
+
* 끌어다 놓은 경로를 터미널에 넣는다 — 순정 Tabby 가 하던 그 일이다.
|
|
590
|
+
*
|
|
591
|
+
* 공백이 든 경로는 따옴표로 감싼다. 그러지 않으면 셸이 두 인자로 쪼개 읽는다.
|
|
592
|
+
* **개행은 붙이지 않는다** — 사용자가 앞에 `cat` 을 붙일 여지를 남긴다.
|
|
593
|
+
*/
|
|
594
|
+
private pasteDroppedPath;
|
|
595
|
+
/**
|
|
596
|
+
* 드롭 확인 배너. **OS 모달이 아니라 인라인 카드**다 —
|
|
597
|
+
* 모달은 터미널 흐름을 끊고, CDP 로 자동 검증할 수 없다(`showMessageBox` 는 프로브가
|
|
598
|
+
* 응답할 방법이 없어 회귀가 그 자리에서 멈춘다).
|
|
599
|
+
*/
|
|
600
|
+
private askFileDrop;
|
|
601
|
+
/**
|
|
602
|
+
* 이 탭에서 도는 에이전트 CLI 가 뭔지 — 상태 감지 패턴과 이미지 붙여넣기 키가 앱마다 다르다.
|
|
326
603
|
*
|
|
327
604
|
* 프로세스 트리를 먼저 본다 (`session.getChildProcesses()`). npm 전역 래퍼를 거치면
|
|
328
605
|
* 커맨드가 node/pwsh 로만 보일 수 있어서, 그때는 탭 제목으로 한 번 더 본다.
|
|
606
|
+
* 어느 조각을 어떤 순서로 보는지는 `agents.ts` 의 프로필이 정한다 — 이 함수에 남은 일은
|
|
607
|
+
* pane 에서 근거(프로세스 이름·제목)를 모으는 것뿐이다.
|
|
608
|
+
*
|
|
609
|
+
* `via` 를 함께 돌려주는 이유: 나중에 "왜 codex 로 잡혔나" 를 가르려면 프로세스에서
|
|
610
|
+
* 나온 판정인지 제목(신뢰도가 낮다)에서 나온 판정인지가 유일한 단서다.
|
|
329
611
|
*/
|
|
330
612
|
private detectAgentApp;
|
|
613
|
+
/**
|
|
614
|
+
* 그 pid 들의 명령줄을 한 줄로 이어 돌려준다 (식별 폴백 전용).
|
|
615
|
+
*
|
|
616
|
+
* 비용이 있는 호출이라 **이름·제목으로 못 가렸을 때만** 부른다. 호출 빈도는 `probeAgent`
|
|
617
|
+
* 의 TTL(`AGENT_PROBE_TTL_MS`)이 잡아 준다 — 출력 조각마다 도는 경로가 아니다.
|
|
618
|
+
*
|
|
619
|
+
* pid 는 **숫자만** 통과시킨다. 값을 명령에 이어 붙이는 자리라, 여기서 걸러내지 않으면
|
|
620
|
+
* 그대로 셸에 넘어간다.
|
|
621
|
+
*/
|
|
622
|
+
private commandLinesOf;
|
|
623
|
+
/**
|
|
624
|
+
* 이 탭의 에이전트를 알아내 기억한다 — 출력 경로에서 프로세스 트리를 묻지 않기 위한 캐시.
|
|
625
|
+
*
|
|
626
|
+
* TTL(`AGENT_PROBE_TTL_MS`)로 묶는다. 세션이 붙는 시점에는 셸만 떠 있고 사용자가 그 뒤
|
|
627
|
+
* `claude` 를 직접 띄우는 경우가 흔해서, 한 번 unknown 이어도 제목이 바뀔 때 다시 본다.
|
|
628
|
+
* 반대로 이미 알아낸 탭이 일시적으로 unknown 으로 읽히면(프로세스 트리 조회 실패 등)
|
|
629
|
+
* 기억을 지우지 않는다 — 지웠다가 합집합으로 되돌아가면 판정이 왔다갔다 한다.
|
|
630
|
+
*/
|
|
631
|
+
private probeAgent;
|
|
632
|
+
/** 그 탭의 첫 pane — 프로세스 트리를 물어볼 대상. 래퍼 탭(SplitTab)이면 자식 중 첫 번째 */
|
|
633
|
+
private firstPane;
|
|
634
|
+
/**
|
|
635
|
+
* 상태 감지에 넘길 프로필 — **아직 모르면 넘기지 않는다.**
|
|
636
|
+
*
|
|
637
|
+
* `undefined` 를 주면 detect 쪽 기본값(합집합)이 쓰이고, 그게 0.5.0 까지의 판정과 같다.
|
|
638
|
+
* 판정 전이나 판정 실패(보통 셸)에 특정 에이전트 프로필을 씌우면, 그 에이전트에 없는
|
|
639
|
+
* 문구를 못 잡아 예전보다 못해진다.
|
|
640
|
+
*/
|
|
641
|
+
/**
|
|
642
|
+
* pane 으로 들어오는 경로(화면 읽기·깨짐 판정)에서 쓰는 프로필.
|
|
643
|
+
*
|
|
644
|
+
* 상태는 root 탭 단위로 들고 있으므로(SplitTab 래퍼) pane 을 부모로 올려서 찾는다 —
|
|
645
|
+
* `decorator.ts` 의 `rootOf` 와 같은 규칙이다. 정규화하지 않으면 자식에 쓰고 부모를 읽어
|
|
646
|
+
* 영영 매칭되지 않는다.
|
|
647
|
+
*/
|
|
648
|
+
private profileForPane;
|
|
649
|
+
private profileForTab;
|
|
331
650
|
/**
|
|
332
651
|
* pane.sendInput 을 한 번 감싸 PTY 로 나간 입력을 진단 로그에 남긴다.
|
|
333
652
|
*
|
|
@@ -457,6 +776,72 @@ export declare class AgentDeckService {
|
|
|
457
776
|
*/
|
|
458
777
|
private nudgePtyRedraw;
|
|
459
778
|
private buildSidebar;
|
|
779
|
+
/**
|
|
780
|
+
* 검색창 배선.
|
|
781
|
+
*
|
|
782
|
+
* 키 이벤트를 여기서 끊는 이유는 `startLabelEdit` 와 같다 — 안 끊으면 터미널로 새어 나간다.
|
|
783
|
+
* 캡처 단계 리스너들(Ctrl+V·Shift+Enter·Enter 라벨)은 `isPlainInput` 이 이 입력창을
|
|
784
|
+
* "터미널이 아닌 보통 입력 필드" 로 판정해 이미 비켜 준다(INPUT 태그 + .xterm 밖).
|
|
785
|
+
*/
|
|
786
|
+
private wireSearch;
|
|
787
|
+
/** 검색어와 상태 필터를 한 번에 지운다 — Esc 와 `✕` 가 같은 곳으로 온다("전체 목록으로") */
|
|
788
|
+
private clearFilters;
|
|
789
|
+
/** 검색어가 있을 때만 `✕` 를 보여준다 — 빈 칸 옆의 지우기 버튼은 무엇을 지우는지 알 수 없다 */
|
|
790
|
+
private syncSearchUi;
|
|
791
|
+
/**
|
|
792
|
+
* 검색 줄을 보일지 (`searchBox` 설정).
|
|
793
|
+
*
|
|
794
|
+
* **끌 때 필터도 같이 지운다.** 입력창만 감추고 검색어를 남기면 목록이 좁혀진 이유가
|
|
795
|
+
* 화면에서 사라진다 — 사용자에게는 탭이 사라진 것과 구분되지 않는다.
|
|
796
|
+
*/
|
|
797
|
+
private applySearchBox;
|
|
798
|
+
/**
|
|
799
|
+
* 상태 칩 클릭 = 그 상태만 보기, 한 번 더 누르면 해제.
|
|
800
|
+
*
|
|
801
|
+
* 왜 새 UI 대신 칩인가 — 칩은 이미 헤더에 있고, 이미 상태별 개수를 급한 순서로 보여준다
|
|
802
|
+
* (`order.ts` STATUS_ORDER). 사용자가 `⏸ 2` 를 보고 "그 둘이 어디 있나" 를 찾는 순간이
|
|
803
|
+
* 곧 필터가 필요한 순간이라, 누를 자리와 알고 싶은 것이 같은 픽셀에 있다.
|
|
804
|
+
* 드롭다운을 새로 두면 같은 정보를 두 군데서 말하게 되고, 200px 짜리 사이드바
|
|
805
|
+
* (`sidebarMin`)에서 검색창 옆에 그것을 놓을 자리도 없다.
|
|
806
|
+
*/
|
|
807
|
+
private toggleStatusFilter;
|
|
808
|
+
/**
|
|
809
|
+
* 이 탭이 지금 걸린 필터를 통과하나.
|
|
810
|
+
*
|
|
811
|
+
* 매칭 후보는 **사용자가 그 줄에서 눈으로 보는 것 + 그 줄이 속한 그룹**이다 —
|
|
812
|
+
* 제목(`customTitle` 우선, `[AD:...]` 표식은 뗀 것), 작업 이름(라벨), 작업 폴더 경로.
|
|
813
|
+
* 그룹 라벨은 폴더 경로의 뒷조각이라(`group.ts` `groupLabelOf`) 경로를 후보에 넣으면
|
|
814
|
+
* 라벨로 검색한 것도 같이 걸린다. cwd 를 아직 모르는 탭은 `기타` 그룹으로 묶이므로
|
|
815
|
+
* 그 이름도 후보에 넣는다 — 화면에 그렇게 적혀 있으니 그것으로 찾을 수 있어야 한다.
|
|
816
|
+
*/
|
|
817
|
+
private tabMatchesFilter;
|
|
818
|
+
/**
|
|
819
|
+
* 옛 미리보기 설정(`viewerFollowMode` 목록 / 그보다 옛 `viewerFollow` boolean)을
|
|
820
|
+
* 토글 두 개(`viewerPreload`/`viewerAutoOpen`)로 옮긴다 — **기동 때 한 번.**
|
|
821
|
+
*
|
|
822
|
+
* 읽는 시점에 옛 값을 보는 방식으로 두지 않은 이유: 토글은 boolean 이라 "아직 안 골랐다" 를
|
|
823
|
+
* 담을 자리가 없다. 빈 값을 그대로 두면 화면에는 꺼져 보이는데 동작은 켜져 있게 된다.
|
|
824
|
+
* 그래서 한 번 적고 **옛 키를 기본값으로 비운다** — 안 비우면 다음 기동에 또 옮기면서
|
|
825
|
+
* 사람이 방금 바꾼 토글을 덮는다.
|
|
826
|
+
*/
|
|
827
|
+
private migrateFollowConfig;
|
|
828
|
+
/**
|
|
829
|
+
* 결과물 미리보기 패널을 만든다 (viewPanel.ts).
|
|
830
|
+
*
|
|
831
|
+
* 사이드바와 같은 방식으로 `.window` 위에 절대 배치하고, 위치·폭은 relayout 이 정한다.
|
|
832
|
+
* 열림 상태는 설정(`viewerOpen`)에 남겨 다음 기동에 그대로 복원한다 — 문서를 띄워 놓고
|
|
833
|
+
* 쓰는 사람에게는 그게 기본 화면이기 때문이다.
|
|
834
|
+
*/
|
|
835
|
+
private buildView;
|
|
836
|
+
/** 미리보기 패널이 붙는 방향 (설정값이 망가져 있으면 오른쪽) */
|
|
837
|
+
private get viewSide();
|
|
838
|
+
/**
|
|
839
|
+
* 이 탭의 작업 디렉토리를 캐시에 채운다.
|
|
840
|
+
*
|
|
841
|
+
* 미리보기 패널이 화면에서 주운 상대경로를 풀 때 쓴다. `getWorkingDirectory()` 는 비동기
|
|
842
|
+
* OS 호출이라 출력 조각마다 부를 수 없으므로 `CWD_TTL_MS` 주기로만 갱신한다.
|
|
843
|
+
*/
|
|
844
|
+
private touchCwd;
|
|
460
845
|
/** 설정에 지정된 기본 프로필(기본값 agentdeck:root)로 새 탭을 연다 */
|
|
461
846
|
private openNewTab;
|
|
462
847
|
private openSettings;
|
|
@@ -484,16 +869,402 @@ export declare class AgentDeckService {
|
|
|
484
869
|
*/
|
|
485
870
|
private disableBackgroundThrottling;
|
|
486
871
|
private tick;
|
|
872
|
+
/**
|
|
873
|
+
* 살아 있는 세션의 라벨·cwd·상태를 원장에 적는다 (매초).
|
|
874
|
+
*
|
|
875
|
+
* **여기가 적는 유일한 자리다.** 세 조각이 각각 다른 곳에 있기 때문이다 —
|
|
876
|
+
* 세션↔탭은 `notify`, 라벨·상태는 `status.service`, cwd 는 이 서비스의 `cwdCache`.
|
|
877
|
+
* 한 곳에서 모아 적지 않으면 셋이 어긋난 원장이 남는다.
|
|
878
|
+
*
|
|
879
|
+
* 매초 파일을 쓰지는 않는다 — `note` 가 값이 그대로면 저장을 예약하지 않고, 예약도 2초로 모은다.
|
|
880
|
+
*/
|
|
881
|
+
private syncLedger;
|
|
882
|
+
/**
|
|
883
|
+
* 사이드바를 **무엇을 어떤 순서로** 그릴지 한 번에 계산한다 (그리지는 않는다).
|
|
884
|
+
*
|
|
885
|
+
* `render()` 와 진단구(`__agentdeck.groups()`)가 **같은 계산을 공유하기 위해** 뽑았다.
|
|
886
|
+
* 예전에는 이 규칙이 `render()` 안에만 있어서, 검증 도구가 헤더 생략 조건을
|
|
887
|
+
* `list.filter(key!==null).length > 1` 로 **다시 적어야** 했다. 그런 사본은 낡는다 —
|
|
888
|
+
* 이 저장소가 `judge()`·`profiles()`·`groups()` 를 연 이유가 전부 그것이다.
|
|
889
|
+
*/
|
|
890
|
+
private renderPlan;
|
|
487
891
|
private render;
|
|
488
892
|
/**
|
|
489
|
-
*
|
|
893
|
+
* 그룹 끝에 **지난 세션** 줄을 붙인다 — `⟲ 지난 세션 5` 한 줄(기본 접힘)과, 펴면 그 목록.
|
|
894
|
+
*
|
|
895
|
+
* 살아 있는 탭 뒤에 오는 이유: 사이드바의 주인공은 지금 도는 작업이고 이건 그 밑의 서랍이다.
|
|
896
|
+
* 검색·상태필터가 걸려 있을 때는 아예 그리지 않는다 — 그 둘은 **탭**을 좁히는 장치라
|
|
897
|
+
* (`tabMatchesFilter`) 죽은 세션에는 적용되지 않는데, 걸러진 화면에 안 걸러진 줄이 섞이면
|
|
898
|
+
* "검색 결과" 라는 말이 깨진다.
|
|
899
|
+
*/
|
|
900
|
+
private renderResume;
|
|
901
|
+
/**
|
|
902
|
+
* 이 줄 묶음이 덮는 그룹 키들.
|
|
903
|
+
*
|
|
904
|
+
* 헤더를 그리는 화면에서는 `group.key` 하나지만, **그룹이 하나뿐이면 사이드바는 헤더를
|
|
905
|
+
* 안 그리고 목록을 평면 한 덩이로 만든다**(`renderPlan`) — 그때 `group.key` 는 `null`(기타)이
|
|
906
|
+
* 되어 어떤 세션도 안 걸린다. 프로젝트를 하나만 열어 둔 화면이 가장 흔하므로 그대로 두면
|
|
907
|
+
* 기능이 평소에 안 보인다. 그래서 **그 덩이에 실제로 들어 있는 탭들의 키**를 모아 쓴다.
|
|
908
|
+
*/
|
|
909
|
+
private resumeKeysOf;
|
|
910
|
+
/** 지금 탭이 들고 있는 세션 — `세션id -> 탭 식별용 문자열`. 되돌아올 탭은 `resumeTabs` 가 들고 있다 */
|
|
911
|
+
private liveSessionIds;
|
|
912
|
+
/** 지난 세션 서랍이 펴져 있나. `collapsedGroups` 와 **반대로 펼침을 적는다** — 기본이 접힘이라서 */
|
|
913
|
+
private isResumeExpanded;
|
|
914
|
+
private toggleResumeExpanded;
|
|
915
|
+
private renderResumeHead;
|
|
916
|
+
private renderResumeRow;
|
|
917
|
+
/**
|
|
918
|
+
* 필터에 걸린 것이 하나도 없을 때의 한 줄.
|
|
919
|
+
*
|
|
920
|
+
* **필터가 걸려 있을 때만** 그린다 — 탭이 정말 0개인 화면(기동 직후)에까지 이 문구를 넣으면
|
|
921
|
+
* 필터와 무관한 상태를 필터 탓으로 읽게 된다. 전체 개수를 같이 적는 이유도 그것이다:
|
|
922
|
+
* "탭은 그대로 있고 내 검색어가 안 걸렸을 뿐" 이 한 줄로 보여야 한다.
|
|
923
|
+
*/
|
|
924
|
+
private renderEmpty;
|
|
925
|
+
/**
|
|
926
|
+
* 탭을 프로젝트(작업 폴더) 그룹으로 나눈다. 규칙은 전부 `group.ts` 에 있다.
|
|
927
|
+
*
|
|
928
|
+
* cwd 는 **이미 채워져 있는 `cwdCache`** 만 본다 — 여기서 `getWorkingDirectory()` 를
|
|
929
|
+
* 새로 부르면 렌더(초당 여러 번, `scheduleRender`)마다 OS 호출이 탭 수만큼 늘어난다.
|
|
930
|
+
* 아직 캐시가 빈 탭은 `기타` 에 있다가, 출력이 흐를 때 `touchCwd` 가 채우면 제 그룹으로 옮겨온다.
|
|
931
|
+
* 프로젝트 루트(`projectRootOf`)도 같은 성질이다 — 처음 보는 폴더는 그 순간엔 cwd 자체가
|
|
932
|
+
* 키였다가, 탐지가 끝나면 저장소 루트로 합쳐진다.
|
|
933
|
+
*
|
|
934
|
+
* 비용은 탭 수만큼의 맵 조작 + 그룹 수²의 라벨 대조뿐이라(group.ts `groupTabs` 주석)
|
|
935
|
+
* 캐시를 두지 않는다 — 캐시 무효화용 시그니처를 만드는 비용이 계산 비용과 같기 때문이다.
|
|
936
|
+
*/
|
|
937
|
+
private groupsFor;
|
|
938
|
+
/** 설정에 남아 있는 접힌 그룹 키들 — 저장·비교는 항상 폴딩한 값으로 (group.ts `collapseIdOf`) */
|
|
939
|
+
private collapsedKeys;
|
|
940
|
+
/**
|
|
941
|
+
* **설정에 저장된** 접힘 여부. 화면에 실제로 접히는지는 `renderPlan().collapseOf` 가 정한다
|
|
942
|
+
* — 검색 중에는 매칭이 있는 그룹을 임시로 펴 보이기 때문이다(group.ts `resolveGroupCollapse`).
|
|
943
|
+
*
|
|
944
|
+
* 이 함수의 의미를 "유효 접힘" 으로 바꾸면 안 된다. 진단구 `groups().list[].collapsed` 와
|
|
945
|
+
* 회귀 GR8·GR9 가 "제품이 **저장값으로** 접힘이라 판정하는가" 를 보고 있어서,
|
|
946
|
+
* 그 값이 필터에 따라 흔들리면 두 항목의 의미가 무너진다.
|
|
947
|
+
*/
|
|
948
|
+
private isGroupCollapsed;
|
|
949
|
+
/**
|
|
950
|
+
* 그룹 접기/펴기. 상태를 설정에 남겨 다음 기동에도 유지한다 —
|
|
951
|
+
* 안 쓰는 프로젝트를 접어 두는 것이 목적인데 매번 다시 접어야 하면 의미가 없다.
|
|
952
|
+
*/
|
|
953
|
+
private toggleGroup;
|
|
954
|
+
/**
|
|
955
|
+
* 그룹 헤더 한 줄 — `▾ tabby-agentdeck 3`. 클릭하면 접힌다.
|
|
956
|
+
*
|
|
957
|
+
* 탭 줄들과 **형제로** 넣는다(그룹마다 감싸는 div 를 두지 않는다). 상/하 도킹은 목록을
|
|
958
|
+
* 가로로 흘리는데(styles.scss `.ad-dock-top .ad-list`), 감싸면 그 flex 흐름이 그룹마다
|
|
959
|
+
* 끊겨 탭이 세로로 쌓인다. 대신 헤더가 CSS 에서 한 줄을 다 차지해(`flex: 0 0 100%`) 줄을 가른다.
|
|
960
|
+
*/
|
|
961
|
+
private renderGroupHead;
|
|
962
|
+
/**
|
|
963
|
+
* 헤더 오른쪽의 상태별 집계 — `⏸ 2 ● 3 ○ 1` 처럼 급한 순서로 놓는다.
|
|
490
964
|
* 총 개수만 보이던 자리다. 세션을 여럿 띄우면 "몇 개가 나를 기다리나" 가 먼저 보여야 한다.
|
|
965
|
+
*
|
|
966
|
+
* 칩은 **상태 필터의 조작부도 겸한다** — 누르면 그 상태만 남고, 한 번 더 누르면 풀린다
|
|
967
|
+
* (그 UI 를 고른 이유는 `toggleStatusFilter` 주석). 그래서 집계는 **항상 전체 탭 기준**이다:
|
|
968
|
+
* 필터 기준으로 세면 다른 상태로 갈아타거나 필터를 풀 수단이 화면에서 사라진다(`render` 주석).
|
|
969
|
+
* 0 인 상태는 그리지 않지만, **지금 걸어 둔 필터의 칩만은 0 이어도 남긴다.**
|
|
491
970
|
* 동적 텍스트는 textContent 로만 넣는다 (innerHTML 금지).
|
|
492
971
|
*/
|
|
493
972
|
private renderHeadCount;
|
|
494
973
|
private renderTab;
|
|
974
|
+
/**
|
|
975
|
+
* 키보드가 지금 어디를 갖고 있나 — **`document.activeElement` 가 유일한 출처다.**
|
|
976
|
+
*
|
|
977
|
+
* 플래그로 들고 있지 않는 이유: "우리가 키를 받는가" 는 곧 브라우저 포커스가 어디 있나이고,
|
|
978
|
+
* 그 둘이 어긋나는 순간(다른 코드가 터미널을 다시 포커스하는 일은 흔하다 —
|
|
979
|
+
* `selectTab` → `emitFocused` → `frontend.focus()`) 플래그를 믿은 쪽이 **터미널 입력을
|
|
980
|
+
* 먹는다.** 파생값으로 두면 어긋날 수가 없다.
|
|
981
|
+
*/
|
|
982
|
+
private get navMode();
|
|
983
|
+
/** 키보드 조작을 받을 상태인가 — 끄면(`keyboardNav: false`) 진입 자체를 하지 않는다 */
|
|
984
|
+
private get navReady();
|
|
985
|
+
/** 검색창이 화면에 있나 (`applySearchBox` 와 같은 조건) — 위쪽 경계에서 나갈 곳이 있는지 */
|
|
986
|
+
private navSearchVisible;
|
|
987
|
+
/**
|
|
988
|
+
* 지금 화면의 이동 순서와 그 안에서의 포커스 위치. **매번 `renderPlan()` 에서 새로 받는다.**
|
|
989
|
+
*
|
|
990
|
+
* 사본을 들고 있지 않는 이유 — 정렬(`sortByStatus`)·검색·상태 필터·그룹 접힘이 전부 이
|
|
991
|
+
* 계산에 걸려 있어서, 한 번 만들어 둔 목록은 다음 상태 변화에 곧 낡는다. 줄 수는 탭 수
|
|
992
|
+
* 규모(수십)라 매 키마다 다시 계산해도 비용이 렌더 한 번보다 싸다.
|
|
993
|
+
*/
|
|
994
|
+
private navPlan;
|
|
995
|
+
/** 같은 줄을 가리키나 — 탭은 객체 동일성, 그룹은 폴딩한 키로 본다(`group.ts collapseIdOf`) */
|
|
996
|
+
private sameNavRow;
|
|
997
|
+
/** 지금 포커스가 이 줄 목록의 몇 번째인가 — 없거나 화면에서 사라졌으면 -1 */
|
|
998
|
+
private navIndexIn;
|
|
999
|
+
/** 이 줄에 포커스 링을 그려야 하나 — **목록이 키보드를 갖고 있을 때만** 그린다 */
|
|
1000
|
+
private isNavFocused;
|
|
1001
|
+
/**
|
|
1002
|
+
* 닫힌 탭을 가리키는 포커스를 버린다 (`render()` 가 매번 부른다).
|
|
1003
|
+
*
|
|
1004
|
+
* **필터로 숨은 줄은 버리지 않는다** — 검색을 지우면 같은 줄로 돌아와야 한다. 그래서 판정
|
|
1005
|
+
* 기준은 `app.tabs` 에 남아 있는지 하나뿐이다(닫힌 탭은 영영 돌아오지 않는다). 이 정리가
|
|
1006
|
+
* 없으면 닫힌 탭 하나를 계속 붙들고 있게 되고, ↑↓ 를 눌러도 "없는 줄" 에서 시작한다.
|
|
1007
|
+
*/
|
|
1008
|
+
private pruneNavFocus;
|
|
1009
|
+
/**
|
|
1010
|
+
* 목록에 키보드 포커스를 준다 (핫키 · 검색창 ↓).
|
|
1011
|
+
*
|
|
1012
|
+
* 포커스를 **`.ad-list` 엘리먼트에** 준다. 이 엘리먼트는 `render()` 가 innerHTML 만 비우고
|
|
1013
|
+
* 자신은 새로 만들지 않으므로(`buildSidebar` 에서 한 번 만든다) 렌더를 몇 번 왕복해도
|
|
1014
|
+
* 포커스가 살아남는다 — 줄(`.ad-tab`)에 포커스를 주면 매 렌더에 날아간다. 어느 줄인지는
|
|
1015
|
+
* DOM 이 아니라 `navFocus`(탭 객체 / 그룹 키)가 들고 있어서 역시 렌더에 지워지지 않는다.
|
|
1016
|
+
* 라벨 편집(`this.editing`)·드래그(`this.drag`)가 대상을 객체로 들고 있는 것과 같은 방식이다.
|
|
1017
|
+
*/
|
|
1018
|
+
private focusList;
|
|
1019
|
+
/** 검색창으로 (목록 첫 줄에서 ↑ · 목록에서 Tab · 핫키인데 줄이 하나도 없을 때) */
|
|
1020
|
+
private focusSearch;
|
|
1021
|
+
/**
|
|
1022
|
+
* 키보드를 터미널에 돌려준다 (Esc · Enter 로 탭을 고른 뒤).
|
|
1023
|
+
*
|
|
1024
|
+
* **먼저 우리 것을 blur 한다.** `frontend.focus()` 는 `setTimeout` 안에서 xterm 을 포커스하므로
|
|
1025
|
+
* (`tabby-terminal/dist/index.js:3415`) 그것만 부르면 이 함수가 끝난 시점에는 아직 포커스가
|
|
1026
|
+
* 우리에게 있고, 곧바로 그리는 화면에 링이 남는다. blur 는 동기라 `navMode` 가 즉시 'off' 가 된다.
|
|
1027
|
+
* blur 만 하고 끝내지 않는 이유도 분명하다 — 포커스가 `<body>` 에 앉으면 사이드바도 터미널도
|
|
1028
|
+
* 키를 못 받아 키보드가 죽은 것처럼 보인다.
|
|
1029
|
+
*/
|
|
1030
|
+
private releaseKeyboard;
|
|
1031
|
+
/**
|
|
1032
|
+
* ↑↓ · Home · End.
|
|
1033
|
+
*
|
|
1034
|
+
* 경계 규칙은 순수 함수(`stepNavIndex`)가 정하고, 여기서는 **검색창으로 나가는 경계**만
|
|
1035
|
+
* 따로 본다: 검색 줄이 보이는 상태에서 첫 줄의 ↑ 는 항상 검색창이다. 감싸기(`keyboardNavWrap`)
|
|
1036
|
+
* 보다 이쪽을 앞에 두는 이유 — 검색↔목록 왕복이 이 기능의 요구사항이고, 감싸기를 켠 사람에게
|
|
1037
|
+
* 그 길이 사라지면 검색창으로 되돌아갈 키가 아예 없어진다(Tab 이 남지만 그건 우회로다).
|
|
1038
|
+
*/
|
|
1039
|
+
private moveNav;
|
|
1040
|
+
/**
|
|
1041
|
+
* Enter — 탭 줄이면 그 탭으로 전환, 그룹 헤더면 접기/펴기.
|
|
1042
|
+
*
|
|
1043
|
+
* **여기가 포커스와 선택이 만나는 유일한 지점이다.** ↑↓ 로는 활성 탭이 바뀌지 않고
|
|
1044
|
+
* (그래야 훑어볼 수 있다, `navFocus` 주석), 사람이 "이거" 라고 말하는 순간이 Enter 다.
|
|
1045
|
+
* 탭을 골랐으면 이어서 할 일은 그 터미널에 치는 것이므로 키보드도 같이 넘긴다.
|
|
1046
|
+
* 헤더에서는 넘기지 않는다 — 접었다 펴 보는 것은 아직 목록을 훑는 중이다.
|
|
1047
|
+
*/
|
|
1048
|
+
private activateNav;
|
|
1049
|
+
/**
|
|
1050
|
+
* Esc — **보이는 것부터 되돌린다.** 걸린 검색어·상태 필터가 있으면 그것을 먼저 지우고
|
|
1051
|
+
* 포커스는 사이드바에 남기고, 지울 것이 없을 때 비로소 터미널로 나간다.
|
|
1052
|
+
*
|
|
1053
|
+
* 순서를 이렇게 정한 근거는 이 기능 밖에 있다 — `✕` 버튼의 title 이 이미
|
|
1054
|
+
* `검색·필터 지우기 (Esc)` 이고 빈 목록 안내도 `Esc 또는 ✕ 로 전체 목록으로 돌아간다` 라고
|
|
1055
|
+
* 적혀 있다(`renderEmpty`). 회귀 R52 도 그 계약을 본다. Esc 를 먼저 "나가기" 로 바꾸면
|
|
1056
|
+
* 화면에 적힌 말과 어긋나고, 좁혀진 목록을 남긴 채 포커스만 빠져 필터가 감춰진다 —
|
|
1057
|
+
* 이 저장소가 가장 위험하다고 적어 둔 실패 모양이다(`searchQuery` 주석).
|
|
1058
|
+
*
|
|
1059
|
+
* 검색창의 Esc 도 이 함수로 온다 — 규칙이 두 군데 있으면 갈린다.
|
|
1060
|
+
*/
|
|
1061
|
+
private navEscape;
|
|
1062
|
+
/**
|
|
1063
|
+
* 핫키(`agentdeck-focus-list`) — 같은 키로 들어가고 나온다.
|
|
1064
|
+
*
|
|
1065
|
+
* 목록으로 들어간다(검색창이 아니라). 이 키를 누르는 목적은 "지금 뜬 세션 중에서 고르는 것"
|
|
1066
|
+
* 이고, 검색은 첫 줄에서 ↑ 한 번이면 닿는다. 반대로 검색창을 진입점으로 삼으면 세션이
|
|
1067
|
+
* 세 개뿐인 사람도 매번 빈 입력창을 지나가야 한다. 줄이 하나도 없으면(탭 0개 · 필터에
|
|
1068
|
+
* 아무것도 안 걸림) 검색창으로 보낸다 — 그때 사람이 할 수 있는 일은 검색어를 고치는 것뿐이다.
|
|
1069
|
+
*/
|
|
1070
|
+
private toggleKeyboardNav;
|
|
1071
|
+
/**
|
|
1072
|
+
* 포커스 줄이 목록 밖으로 나갔으면 스크롤해 보여준다.
|
|
1073
|
+
*
|
|
1074
|
+
* **키로 옮겼을 때만** 부른다. 매 렌더(경과시간 tick 이 1초마다 돈다)마다 부르면 마우스로
|
|
1075
|
+
* 목록을 스크롤해 둔 사람의 화면을 계속 되돌린다. `block: 'nearest'` 라 이미 보이는 줄에는
|
|
1076
|
+
* 아무 일도 하지 않으므로, 목록이 길어져도 필요한 만큼만 움직인다
|
|
1077
|
+
* (탭이 많으면 화면 밖으로 못 나가던 것이 cycle 10 에서 남은 것으로 적혀 있었다).
|
|
1078
|
+
*/
|
|
1079
|
+
private scrollNavIntoView;
|
|
1080
|
+
/**
|
|
1081
|
+
* 목록의 키보드 조작을 배선한다.
|
|
1082
|
+
*
|
|
1083
|
+
* 리스너를 **`.ad-list` 엘리먼트에** 건다 — 그것이 곧 "포커스가 없을 때는 아무것도
|
|
1084
|
+
* 가로채지 않는다" 의 보증이다. document 리스너 + 플래그로 하면 플래그가 어긋나는 순간
|
|
1085
|
+
* 터미널 입력을 먹는데, 이 파일은 이미 그 대가를 알고 있다(`claimCtrlVKey` 계열은 그래서
|
|
1086
|
+
* `isPlainInput` 가드를 달고 있다). 엘리먼트에 걸면 포커스가 여기 없는 동안 이벤트가
|
|
1087
|
+
* **아예 오지 않는다.**
|
|
1088
|
+
*
|
|
1089
|
+
* 처리한 키는 `preventDefault` + `stopPropagation` 으로 끊는다. 끊으면 Tabby core 의
|
|
1090
|
+
* document **버블** 리스너(`tabby-core/dist/index.js:6329` keydown → `pushKeyEvent`)까지
|
|
1091
|
+
* 죽으므로 핫키도 발화하지 않는다 — 우리 리스너는 target(`.ad-list`)에 걸려 있어 그보다
|
|
1092
|
+
* 먼저 돈다. 반대로 **캡처 단계**에 걸린 우리 리스너들(Ctrl+V · Shift+Enter · Enter 라벨 ·
|
|
1093
|
+
* Home/End IME)은 이보다 먼저 돌아 끊을 수 없으므로, 그쪽이 `isDeckUiKey` 로 사이드바에서
|
|
1094
|
+
* 난 키를 비켜 준다.
|
|
1095
|
+
*
|
|
1096
|
+
* 수식키(Ctrl·Alt·Meta)가 붙은 조합은 손대지 않는다 — 그건 Tabby 핫키의 몫이다
|
|
1097
|
+
* (Shift 는 예외: Shift+Tab 이 Tab 과 같은 뜻이어야 한다).
|
|
1098
|
+
*/
|
|
1099
|
+
private wireListKeys;
|
|
1100
|
+
/**
|
|
1101
|
+
* pointerdown — 아직 드래그로 단정하지 않는다(무장까지만).
|
|
1102
|
+
*
|
|
1103
|
+
* 이 줄 위에서 일어나는 조작이 이미 넷이다: 클릭=탭 선택, 더블클릭=라벨 편집,
|
|
1104
|
+
* 우클릭=상태 지정, `✕`=닫기. 그래서 **왼쪽 버튼 + 버튼/입력창 밖 + 임계치 초과** 세
|
|
1105
|
+
* 조건을 다 만족할 때만 드래그가 된다 (`dock.ts installDockDrag` 와 같은 규약).
|
|
1106
|
+
*
|
|
1107
|
+
* 뒤 이벤트를 줄이 아니라 **document 에서 캡처 단계로** 받는 이유 둘 —
|
|
1108
|
+
* 1) `setPointerCapture` 는 합성 포인터에서 던진다(tools/README 함정 목록). 캡처에
|
|
1109
|
+
* 기대면 회귀 프로브가 이 기능을 흉내낼 수 없다. document 리스너는 캡처가 없어도
|
|
1110
|
+
* 커서가 줄 밖으로 나가도 계속 온다.
|
|
1111
|
+
* 2) Esc 를 캡처 단계에서 먼저 잡아야 터미널·검색창으로 새지 않는다.
|
|
1112
|
+
*/
|
|
1113
|
+
private armRowDrag;
|
|
1114
|
+
/** pointermove — 임계치를 넘기면 드래그로 승격하고, 그 뒤로는 놓일 자리를 계속 다시 잰다 */
|
|
1115
|
+
private dragMove;
|
|
1116
|
+
/**
|
|
1117
|
+
* 지금 포인터 자리로 놓일 자리를 다시 재고 삽입선을 다시 그린다.
|
|
1118
|
+
*
|
|
1119
|
+
* 포인터가 움직였을 때(`dragMove`)와 **목록이 커서 아래에서 흘렀을 때**(`dragScrollTick`)
|
|
1120
|
+
* 둘 다 필요한 일이라 한 곳에 둔다. 스크롤 뒤 이것을 부르지 않으면 삽입선이 옛 자리에
|
|
1121
|
+
* 남아 "어디에 떨어질지 모르는 드래그" 가 되어, 자동 스크롤이 없애려던 문제로 돌아간다.
|
|
1122
|
+
*
|
|
1123
|
+
* 판정은 반드시 `dropTargetAt` 을 지난다 — 낡은 `data-ad-index` 검사(`stale-index`)를
|
|
1124
|
+
* 우회하는 지름길을 만들지 않는다.
|
|
1125
|
+
*/
|
|
1126
|
+
private refreshDropTarget;
|
|
1127
|
+
/**
|
|
1128
|
+
* 목록이 실제로 스크롤되는 축 — 도킹 방향으로 정하지 않고 **넘치는 쪽을 재서** 정한다.
|
|
1129
|
+
* 넘치지 않으면 null (= 자동 스크롤이 할 일이 없는 화면).
|
|
1130
|
+
*
|
|
1131
|
+
* 흐르는 축(`listFlowsSideways`)과 **스크롤되는 축이 다르다**는 것이 실측이다:
|
|
1132
|
+
* 상/하 도킹의 목록은 줄이 가로로 흐르지만 `flex-wrap: wrap` + `overflow-x: hidden`
|
|
1133
|
+
* 이라 넘치면 **아래로 감기고 세로로** 스크롤된다(`styles.scss` 의 `.ad-dock-top .ad-list`).
|
|
1134
|
+
* 그래서 흐름축을 그대로 스크롤축으로 쓰면 상/하 도킹에서 이 기능이 **조용히 아무것도
|
|
1135
|
+
* 안 하게** 된다. 흐름축은 두 축이 다 넘칠 때의 우선순위로만 쓴다 — 그때는 사용자가
|
|
1136
|
+
* 눈으로 따라가는 방향이 흐름축이다.
|
|
1137
|
+
*/
|
|
1138
|
+
private listScrollAxis;
|
|
1139
|
+
/**
|
|
1140
|
+
* 지금 포인터 자리에서의 자동 스크롤 속도 (px/s). 0 이면 스크롤할 이유가 없다.
|
|
1141
|
+
*
|
|
1142
|
+
* **여기는 DOM 을 읽는 껍데기고 판단은 `planEdgeScroll` 이 한다** — 속도 램프·띠 겹침·
|
|
1143
|
+
* 끝 도달 같은 규칙이 rect 읽기와 한 함수에 있으면 그것을 검사할 방법이 없다.
|
|
1144
|
+
* 옮긴 뒤 `test/order.test.js` 가 전수로 잰다(`planTabMove`·`stepNavIndex` 와 같은 방식).
|
|
1145
|
+
*/
|
|
1146
|
+
private dragScrollSpeed;
|
|
1147
|
+
/**
|
|
1148
|
+
* 포인터가 목록 가장자리 띠에 있으면 자동 스크롤 루프를 건다 (이미 돌고 있으면 그대로 둔다).
|
|
1149
|
+
*
|
|
1150
|
+
* **끌고 있을 때만 부른다** — `dragMove` 가 임계치를 넘긴 뒤의 유일한 호출자다.
|
|
1151
|
+
* 마우스가 목록 끝에 얹혀 있어도 드래그가 아니면 이 함수에 닿는 길이 없다(hover 리스너를
|
|
1152
|
+
* 두지 않은 이유가 그것이다).
|
|
1153
|
+
*/
|
|
1154
|
+
private armDragScroll;
|
|
1155
|
+
/**
|
|
1156
|
+
* 자동 스크롤 한 프레임 — 목록을 조금 흘리고 **놓일 자리를 다시 잰다**.
|
|
1157
|
+
*
|
|
1158
|
+
* `render()` 를 부르지 않는다(부르면 끌고 있던 줄의 DOM 이 사라진다 — `render` 의 드래그
|
|
1159
|
+
* 가드). 여기서 만지는 것은 목록의 `scrollTop`/`scrollLeft` 와 삽입선뿐이다.
|
|
1160
|
+
* `scrollNavIntoView` 도 부르지 않는다 — 그건 키 이동의 몫이고, 여기서 부르면 방금
|
|
1161
|
+
* 흘린 화면을 포커스 줄 기준으로 되돌린다.
|
|
1162
|
+
*/
|
|
1163
|
+
private dragScrollTick;
|
|
1164
|
+
/** 자동 스크롤 루프를 끊는다 — 취소 없이 두면 드래그가 끝나도 프레임마다 계속 돈다 */
|
|
1165
|
+
private stopDragScroll;
|
|
1166
|
+
/**
|
|
1167
|
+
* 이 좌표에 놓으면 어디로 가나 — 놓을 자리가 없으면 null(= 놓아도 아무 일 없음).
|
|
1168
|
+
*
|
|
1169
|
+
* **후보 집합을 `renderPlan()` 에서 받는다.** 화면 순서를 정하는 계산이 한 곳뿐이어야
|
|
1170
|
+
* 진단구(`__agentdeck.groups()`)와 화면이 어긋나지 않는다 — 이 파일에서 순서 규칙의
|
|
1171
|
+
* 사본을 만들지 않는 것이 그 함수를 뽑은 이유다.
|
|
1172
|
+
*
|
|
1173
|
+
* 후보는 **끌고 있는 탭이 속한 `plan` 그룹의 탭들**이다. 두 화면이 한 규칙으로 덮인다 —
|
|
1174
|
+
* - 헤더가 없는 화면: `plan` 이 평면 한 덩이라(renderPlan) 후보 = 보이는 줄 전부.
|
|
1175
|
+
* - 헤더가 있는 화면: 후보 = **같은 그룹 안**. 그룹 키는 작업 폴더(cwd)라 순서를 바꿔도
|
|
1176
|
+
* 그룹이 바뀌지 않으므로, 다른 그룹의 줄에 놓아 주면 그 줄은 사용자가 놓은 자리가 아니라
|
|
1177
|
+
* 자기 그룹 안으로 되돌아가 앉는다. "놓은 곳에 안 간다" 가 가장 나쁜 결과라, 다른 그룹
|
|
1178
|
+
* 위에서는 삽입선을 아예 그리지 않고 놓아도 취소로 끝낸다(무시).
|
|
1179
|
+
*
|
|
1180
|
+
* 필터가 걸려 줄이 숨은 화면에서는 숨은 탭이 사이에 남는다 — 그래도 **보이는 줄들의
|
|
1181
|
+
* 상대 순서**는 사용자가 놓은 그대로가 된다. 눈에 보이지 않는 탭의 자리를 짐작해 옮기는
|
|
1182
|
+
* 것보다 이쪽이 예측 가능하다.
|
|
1183
|
+
*/
|
|
1184
|
+
private dropTargetAt;
|
|
1185
|
+
/** 목록이 가로로 흐르나 (상/하 도킹) — 삽입선의 방향과 중점 판정 축이 여기서 갈린다 */
|
|
1186
|
+
private listFlowsSideways;
|
|
1187
|
+
/**
|
|
1188
|
+
* 놓일 자리를 삽입선으로 보여준다 (요구: 어디에 떨어질지 모르는 드래그는 쓸 수 없다).
|
|
1189
|
+
*
|
|
1190
|
+
* **`.ad-list` 안에 넣지 않는다.** 목록의 자식으로 두면 (1) `render()` 가 innerHTML 을
|
|
1191
|
+
* 비울 때 같이 날아가고, (2) 회귀 프로브가 목록의 자식을 순서대로 훑어 그룹 구성을 읽는데
|
|
1192
|
+
* (probe-group `sections`) 그 사이에 낯선 노드가 끼어든다. dock.ts 의 드롭 가이드와 같이
|
|
1193
|
+
* body 에 붙이고 `position: fixed` 로 화면 좌표에 그린다 — 사이드바의 `overflow: hidden`
|
|
1194
|
+
* 에도 잘리지 않는다.
|
|
1195
|
+
*/
|
|
1196
|
+
private drawReorderLine;
|
|
1197
|
+
/** 그 탭의 줄 엘리먼트 — 줄은 `data-ad-index`(= `app.tabs` 인덱스) 로만 찾는다 */
|
|
1198
|
+
private rowElOf;
|
|
1199
|
+
/**
|
|
1200
|
+
* 드래그 종료 — `commit` 이 false 면 취소(원래 순서 그대로).
|
|
1201
|
+
*
|
|
1202
|
+
* 취소로 들어오는 길은 셋이다: Esc, `pointercancel`, 그리고 놓을 자리가 없는 곳에서
|
|
1203
|
+
* 손을 뗀 것(목록 밖 · 다른 그룹 위). 셋 다 `app.tabs` 를 건드리지 않고 화면만 되돌린다.
|
|
1204
|
+
*/
|
|
1205
|
+
private endRowDrag;
|
|
1206
|
+
/**
|
|
1207
|
+
* `app.tabs` 를 실제로 재배열한다 — **순정과 같은 방법**이다.
|
|
1208
|
+
*
|
|
1209
|
+
* 근거: Tabby 의 탭바도 드롭을 받으면 그 배열을 제자리에서 흔들고 변경을 알린다 —
|
|
1210
|
+
* `moveItemInArray(this.app.tabs, event.previousIndex, event.currentIndex); this.app.emitTabsChanged()`
|
|
1211
|
+
* (`node_modules/tabby-core/dist/index.js` 의 `onTabsReordered`). 탭바 템플릿도
|
|
1212
|
+
* `*ngFor="let tab of app.tabs; let idx = index"` 로 그 배열을 그대로 읽고 trackBy 가 없어서,
|
|
1213
|
+
* 제자리 재배열 + 변경 감지가 곧 순정 탭바의 순서 변경이다.
|
|
1214
|
+
*
|
|
1215
|
+
* `AppService` 의 순서 API 는 이 조작에 못 쓴다 — `swapTabs(a, b)` 는 두 탭을 맞바꾸기만 해서
|
|
1216
|
+
* "사이에 끼워넣기" 를 표현할 수 없고(중간 탭들이 밀리지 않는다), `moveSelectedTabLeft/Right`
|
|
1217
|
+
* 는 활성 탭만 한 칸씩 옮긴다. 그래서 순정 탭바가 자기 드롭에 쓰는 경로를 그대로 쓴다.
|
|
1218
|
+
*
|
|
1219
|
+
* 배열 객체를 새것으로 갈아 끼우지 않는 이유도 같다 — 이미 `app.tabs` 를 들고 있는 곳이
|
|
1220
|
+
* 있어도 같은 배열을 보게 해야 한다(순정도 제자리에서 흔든다).
|
|
1221
|
+
* 포인터 이벤트는 Angular 밖이라 `zone.run` 안에서 바꿔 변경 감지를 태운다.
|
|
1222
|
+
*/
|
|
1223
|
+
private applyReorder;
|
|
1224
|
+
/**
|
|
1225
|
+
* "지금은 순서를 바꿀 수 없다" 를 그 자리에서 한 줄로 알린다.
|
|
1226
|
+
*
|
|
1227
|
+
* `sortByStatus` 가 켜져 있으면 화면 순서를 상태가 정한다(`renderPlan`) — 옮겨 놓아도
|
|
1228
|
+
* 다음 렌더에 제자리로 돌아가므로 드래그가 무의미하다. 그래서 **막는다.**
|
|
1229
|
+
* 드래그를 계기로 그 설정을 대신 꺼 주지는 않는다: 사용자가 저장해 둔 설정을 곁동작으로
|
|
1230
|
+
* 뒤집는 것은 예측할 수 없고(다음 기동까지 남는다), 무엇이 바뀌었는지 화면에 단서도 없다.
|
|
1231
|
+
* 대신 막은 이유와 푸는 방법을 적어 보여준다.
|
|
1232
|
+
*/
|
|
1233
|
+
private noteReorderBlocked;
|
|
495
1234
|
/** 더블클릭 -> 작업 이름 인라인 편집 */
|
|
496
1235
|
private startLabelEdit;
|
|
1236
|
+
/**
|
|
1237
|
+
* 지난 세션 한 줄을 누른 결과 — **이미 열려 있으면 이어받지 않고 그 탭으로 보낸다.**
|
|
1238
|
+
*
|
|
1239
|
+
* 같은 세션을 두 탭에서 `--resume` 하면 두 프로세스가 같은 기록 파일에 덧쓴다.
|
|
1240
|
+
* 세션↔탭 매핑은 `notify` 가 이미 들고 있어 판정에 드는 비용이 없으니 막는다.
|
|
1241
|
+
*/
|
|
1242
|
+
private activateResume;
|
|
1243
|
+
/**
|
|
1244
|
+
* 이어받기용 새 탭.
|
|
1245
|
+
*
|
|
1246
|
+
* **프로필의 `command` 를 갈아끼우지 않고, 평소 프로필로 셸을 띄운 뒤 명령을 써 넣는다.**
|
|
1247
|
+
* 이유는 `sessionLedger.ts` 의 `resumeCommand` 주석에 있다 — 기존 `agentdeck:root` 프로필의
|
|
1248
|
+
* 인자(`-ExecutionPolicy Bypass`)가 npm 전역 `.ps1` 래퍼 문제를 이미 풀어 두었고,
|
|
1249
|
+
* claude 가 끝나도 셸이 남아 사람이 이어서 쓸 수 있다.
|
|
1250
|
+
*
|
|
1251
|
+
* **cwd 는 그 세션의 것으로 덮는다.** `--resume` 은 cwd 로 세션을 묶기 때문에(같은 세션도
|
|
1252
|
+
* 다른 폴더에서 띄우면 목록에 없다) 여기를 틀리면 이어받기 자체가 실패한다.
|
|
1253
|
+
*/
|
|
1254
|
+
private openResumeTab;
|
|
1255
|
+
/**
|
|
1256
|
+
* 갓 뜬 셸에 명령을 써 넣는다.
|
|
1257
|
+
*
|
|
1258
|
+
* **바로 쏘면 삼켜진다** — pty 는 열렸는데 셸이 아직 프롬프트를 안 그린 상태에서 쓴 바이트는
|
|
1259
|
+
* 어디에도 안 남는다(같은 이유로 `sendInput` 을 쓰는 다른 경로들도 프론트엔드 준비를 기다린다).
|
|
1260
|
+
* 그래서 출력이 한 번이라도 흐른 뒤에 보내고, 끝내 안 오면 정해진 시각에 한 번은 보낸다 —
|
|
1261
|
+
* 아무것도 안 보내고 조용히 끝나는 것이 제일 나쁜 결과다.
|
|
1262
|
+
*/
|
|
1263
|
+
private sendResumeCommand;
|
|
1264
|
+
/** 지난 세션 줄 우클릭 */
|
|
1265
|
+
private showResumeMenu;
|
|
1266
|
+
/** 목록에서만 감춘다 — 기록 파일은 건드리지 않는다(그건 사람이 지울 일이다) */
|
|
1267
|
+
private hideResumeRow;
|
|
497
1268
|
/** 우클릭 -> 상태 직접 지정 */
|
|
498
1269
|
private showStatusMenu;
|
|
499
1270
|
private formatElapsed;
|