tabby-agentdeck 1.0.0 → 1.1.2

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 CHANGED
@@ -37,7 +37,8 @@ Windows 에서 만들고 검증했다. 상태 통보 훅은 PowerShell 스크립
37
37
  | **변경 보기** | 같은 패널의 `변경` 탭이 작업트리의 `git diff` 를 보여준다 — 파일 목록 · `+N -M` · 줄번호 붙은 유니파이드 diff. 누가 고쳤는지와 무관하게 성립한다 |
38
38
  | **`파일:라인` 참조** | diff 나 코드 보기에서 줄을 누르면 그 위치를 터미널에 넣는다(Shift+클릭=범위). "이 줄 고쳐" 를 타이핑 없이 |
39
39
  | **스테이지 · 커밋** | `변경` 탭에서 파일 딱지를 눌러 `git add`/`unstage` 하고, 메시지를 적어 커밋한다. **두 번 눌러야 커밋된다**(첫 누름은 확인 대기) |
40
- | **세션 그룹** | 탭을 작업 폴더(프로젝트)별로 묶고 접는다. 그룹이 하나뿐이면 헤더를 그리지 않고 순서도 건드리지 않는다 |
40
+ | **세션 그룹** | 탭을 프로젝트별로 묶고 접는다. 경계는 저장소 루트(`.git` 등)를 위로 훑어 자동으로 잡으므로 설정이 없다. 그룹이 하나뿐이면 헤더를 그리지 않고 순서도 건드리지 않는다 |
41
+ | **지난 세션 이어받기** | 그룹 안 `⟲ 지난 세션` 을 펴면 **닫힌 Claude Code 세션**이 사람이 친 프롬프트로 보인다. 누르면 그 세션의 작업 폴더에서 `claude --resume` 으로 대화가 되살아난다 |
41
42
  | **한글 조합 보정** | 조합 중인 음절이 커서를 따라가거나 새 줄로 내려가지 않게, pty 에 쓰기 전에 조합이 끝나기를 기다린다 |
42
43
  | **에이전트 프로필** | 어느 CLI 가 도는 탭인지 알아내고(프로세스 → 제목) 그에 맞는 규칙을 쓴다. 새 CLI 지원은 프로필 한 덩이 추가로 끝난다 |
43
44
  | **마우스 도킹** | 헤더를 끌어 좌/우/상/하 어느 가장자리에나 붙이고, 경계선을 끌어 크기를 바꾼다 |
@@ -47,8 +48,9 @@ Windows 에서 만들고 검증했다. 상태 통보 훅은 PowerShell 스크립
47
48
  | **작업 이름** | Enter 로 프롬프트를 보내는 순간 입력창 원문을 읽어 라벨로 삼는다 |
48
49
  | **Claude Code 훅 연동** | 설정 창에서 한 번 설치하면 에이전트가 상태를 직접 알려준다 (선택) |
49
50
  | **기본 프로필** | 새 탭을 항상 지정한 작업 루트에서 연다 |
51
+ | **자동 업데이트** | 기동할 때 npm 을 보고 새 버전이 있으면 물어본 뒤, Tabby 를 닫고 → 설치하고 → **스스로 다시 켠다**. 개발 설치(소스 링크)에는 손대지 않는다 |
50
52
  | **입력 보정** | `Ctrl+V` 이미지 붙여넣기, `Ctrl+Enter`/`Shift+Enter` 줄바꿈, 우클릭 복사/붙여넣기 |
51
- | **화면 복구** | 깨진 TUI 를 지우고 다시 그리게 한다. 사이드바 `↻` / 탭 우클릭 / `agentdeck-repair` 핫키 |
53
+ | **화면 복구** | 깨진 TUI 의 **입력창 잔상만** 지우고 크기를 맞춰 다시 그리게 한다 — 보고 있던 대화는 건드리지 않는다. 사이드바 `↻` / 탭 우클릭 / `agentdeck-repair` 핫키 |
52
54
 
53
55
  ## 설치
54
56
 
@@ -65,6 +67,59 @@ powershell -NoProfile -ExecutionPolicy Bypass -File install.ps1
65
67
  `install.ps1` 은 Tabby 플러그인 디렉토리(`%APPDATA%\tabby\plugins\node_modules\tabby-agentdeck`)에
66
68
  이 폴더를 가리키는 junction 을 건다. 소스를 고치고 `npm run build` 하면 Tabby 재시작으로 반영된다.
67
69
 
70
+ ### 자동 업데이트
71
+
72
+ 기동하고 8초 뒤, npm 레지스트리에 `tabby-agentdeck` 의 최신 버전을 묻는다.
73
+ 새 버전이 있으면 **물어본다** — `지금 업데이트` / `나중에` / `다시 묻지 않기`.
74
+ 승낙하면 이렇게 흐른다:
75
+
76
+ ```
77
+ Tabby 가 닫힌다 → (밖에서) npm install → Tabby 가 스스로 다시 켜진다
78
+ ```
79
+
80
+ **사람이 할 일은 처음 한 번 누르는 것뿐이다.** 설치가 끝나면 알아서 다시 켜진다.
81
+ 설치가 실패해도 Tabby 는 반드시 다시 켠다 — 업데이트를 고르느라 터미널을 잃는 일은 없어야 한다.
82
+ 기록은 `%LOCALAPPDATA%\tabby-agentdeck\update.log` 에 남는다.
83
+
84
+ | 설정 | 기본값 | |
85
+ |---|---|---|
86
+ | `autoUpdate` | `true` | 끄면 확인도 하지 않는다 |
87
+ | `updateCheckIntervalHours` | `6` | 기동마다 묻지 않는다 |
88
+ | `updateRegistry` | `https://registry.npmjs.org` | 사내 미러를 쓰면 바꾼다 |
89
+ | `updateSkipVersion` | — | `다시 묻지 않기` 를 누른 버전. **그 버전만** 건너뛰고 다음 버전은 다시 묻는다 |
90
+
91
+ > **왜 Tabby 를 닫고 까나.** 두 가지 때문이다. ① 돌고 있는 플러그인은 자기를 갈아끼울 수 없다 —
92
+ > Tabby 는 기동할 때 `dist/index.js` 를 한 번 읽어 메모리에 올리므로, 디스크를 바꿔도 돌던 코드는
93
+ > 옛것 그대로다. ② Windows 는 로드된 파일을 잡고 있어서, 떠 있는 동안 npm 이 그 폴더를 지우려 들면
94
+ > 반쯤 지운 채 실패할 수 있다 — 플러그인이 깨진 채 남는 것이 가장 나쁜 결과다.
95
+ > 그래서 설치는 임시 폴더로 떼어낸 `updater/agentdeck-update.ps1` 이 **Tabby 종료를 확인한 뒤** 한다.
96
+ > 기다리는 대상은 **업데이트를 건 그 인스턴스의 메인 프로세스 PID** 하나다 — 이름으로 전부 세면
97
+ > 다른 Tabby 창 하나 때문에 업데이트가 영영 안 된다.
98
+
99
+ > **개발 설치에는 설치하지 않는다.** 플러그인 폴더의 `tabby-agentdeck` 이 소스 트리를 가리키는
100
+ > 링크일 때(`install.ps1` 로 깐 개발 환경), 거기에 `npm install` 이 돌면 **작업 중인 소스를 덮어쓴다.**
101
+ > `src/`·`.git`·`webpack.config.js` 중 하나라도 보이거나 항목이 링크면 새 버전이 있다는 것만 알리고
102
+ > 손대지 않는다 (판정은 `update.ts` 의 `canSelfUpdate`).
103
+
104
+ ### 버전은 한 곳에만 적는다
105
+
106
+ **`package.json` 의 `version` 이 유일한 출처다.** 나머지는 전부 거기서 파생된다 —
107
+ `npm run build` 가 매번 `tools/sync-version.js` 를 먼저 돌려(`prebuild`) `package-lock.json` 과
108
+ 사용설명서(`docs/guide/index.html` 의 `<span class="ad-version">`)를 맞추고,
109
+ 진단 로그의 `plugin=` 은 런타임에 `package.json` 을 읽는다(`diag.ts` `pluginVersion`).
110
+
111
+ ```powershell
112
+ npm run sync-version # 지금 맞추기
113
+ node tools/sync-version.js --check # 어긋난 곳만 알리기 (어긋나면 종료코드 1)
114
+ ```
115
+
116
+ `test/version.test.js` 가 어긋남을 잡으므로 빌드를 안 거친 수정도 `npm test` 에서 걸린다.
117
+ **문서·주석의 `0.18.0 부터 …` 같은 이력 표기는 대상이 아니다** — 그건 "지금 버전" 이 아니라
118
+ 그 기능이 들어온 시점이라 갱신하면 오히려 거짓이 된다.
119
+
120
+ > 이 규칙이 생긴 이유: 2026-09-11 에 같은 버전을 네 곳이 각자 적고 있었고 **전부 달랐다**
121
+ > (`package.json` 1.0.0 / `package-lock.json` 0.3.0 / 사용설명서 0.17.0 / 커밋된 값 0.17.0).
122
+
68
123
  ## 작업 이름과 상태
69
124
 
70
125
  사이드바 한 줄은 **작업 이름(라벨)** 과 **상태 배지** 로 이루어진다.
@@ -144,6 +199,54 @@ Claude Code 가 백그라운드로 에이전트를 띄우면 그 개수가 줄
144
199
  OS 토스트(`alertToast`)는 기본으로 꺼져 있다 — 켜면 토스트를 눌러 바로 그 탭으로 갈 수 있다.
145
200
  창이 앞에 있을 때는 셋 다 아무 일도 하지 않는다. 사이드바가 이미 보이고 있기 때문이다.
146
201
 
202
+ ## 지난 세션 이어받기
203
+
204
+ Tabby 가 꺼져도 **Claude Code 대화는 디스크에 남는다** (`~/.claude/projects/<프로젝트>/<세션id>.jsonl`).
205
+ `claude --resume <세션id>` 로 그대로 이어받을 수 있는데, 실제로 막히는 곳은 명령이 아니라
206
+ **UUID 목록에서 내 작업을 찾는 것**이다.
207
+
208
+ agentdeck 은 그 답을 이미 갖고 있다 — Enter 를 칠 때 입력창에서 주운 프롬프트 원문.
209
+ 그래서 그룹 끝에 서랍을 하나 둔다:
210
+
211
+ ```
212
+ ▾ Root ⏸1 ●2
213
+ ● Agent Root 진행중 ❖2
214
+ 시즌패스 보상 지급경로 정리 3분
215
+ ⟲ 지난 세션 5 ▸ ← 기본 접힘. 누르면 펴진다
216
+ ```
217
+
218
+ 편 모습:
219
+
220
+ ```
221
+ ⟲ 지난 세션 5 ▾
222
+ 백그라운드에서 뭐 돌고있나본데 이거 뭐야
223
+ 완료 14:12
224
+ 시즌패스 미수령 보상 우편지급 전환
225
+ 한도 도달 어제
226
+ ```
227
+
228
+ | 조작 | 하는 일 |
229
+ |---|---|
230
+ | 클릭 | **그 세션의 작업 폴더**에서 새 탭을 열고 `claude --resume <세션id>` 를 보낸다 |
231
+ | 우클릭 → `분기해서 이어받기` | `--fork-session`. 원본 기록을 그 시점 그대로 두고 **복사본**에서 이어간다 — 같은 지점에서 다른 방향을 시도할 때만 쓴다 (매번 쓰면 기록이 불어난다) |
232
+ | 우클릭 → `세션 ID 복사` · `목록에서 숨기기` | 목록에서만 감춘다. 기록 파일은 건드리지 않는다 |
233
+
234
+ 알아 둘 것:
235
+
236
+ - **작업 폴더가 다르면 목록에 없다.** `--resume` 은 cwd 로 세션을 묶으므로 이어받기도 그 폴더에서 연다.
237
+ - **이미 열려 있는 세션**은 `● 열림` 으로 보이고, 누르면 이어받지 않고 그 탭으로 간다 —
238
+ 같은 세션을 두 탭에서 이어받으면 두 프로세스가 한 기록 파일에 덧쓴다.
239
+ - **대화만 돌아온다.** 그때 띄워 둔 로컬 서버나 백그라운드 프로세스는 되살아나지 않는다.
240
+ - 죽은 줄은 색이 아니라 **형태**(점선 바 + 흐림)로 구분한다. 상태 6색은 이미 살아 있는 탭이 쓰고 있어서,
241
+ 일곱 번째 색을 들이면 배지가 뜻하는 바가 흐려진다.
242
+ - 라벨은 두 곳에서 온다 — 살아 있을 때 주운 프롬프트(원장, `%LOCALAPPDATA%\tabby-agentdeck\sessions.json`)와
243
+ 기록 파일 앞부분에서 읽은 첫 프롬프트. 앞엣것이 우선이고, 플러그인을 깔기 전 세션이나 다른 터미널에서
244
+ 띄운 세션은 뒤엣것으로 잡힌다.
245
+ - 기록 파일이 사라진 세션은 목록에서도 원장에서도 지운다. 이어받아도 복원할 것이 없기 때문이다.
246
+
247
+ 이 기능이 있는 이유는 아래 `기동 시 탭 복원 끄기` 와 한 쌍이다 — 순정 복원은 **셸만** 되살리고
248
+ 그 안의 세션은 이미 죽어 있어서 빈 프롬프트 탭만 줄줄이 남는다. 그래서 그쪽은 끄고, 진짜 복원을 여기 둔다.
249
+
147
250
  ## 결과물 미리보기 패널
148
251
 
149
252
  에이전트가 만든 것이 문서나 그림이면 터미널 안에서는 확인할 수 없다. 그래서 탐색기로 나가서 열게 되고,
@@ -327,8 +430,14 @@ OS 토스트(`alertToast`)는 기본으로 꺼져 있다 — 켜면 토스트를
327
430
  ### 화면 복구 (`↻`)
328
431
 
329
432
  statusline 이 사라지거나 입력창 구분선이 화면 중간에서 끊기는 등 화면이 깨졌을 때 누른다.
330
- 폭 재계산 → pty 크기 동기화 → 터미널 상태 초기화 → 앱에 전체 재렌더 요청(`Ctrl+L`) 순으로 진행한다.
331
- 기본값은 스크롤백을 보존한다(`repairHard: false`). 잔상까지 지우려면 `repairHard: true`.
433
+ 폭 재계산 → pty 크기 동기화 → **입력창 자리의 잔상만** 지우기 → 흔들어 SIGWINCH 주기 순으로 진행한다.
434
+ 지운 자리는 앱이 자기 렌더 주기에 다시 그린다.
435
+
436
+ **보고 있던 화면은 지우지 않는다.** 예전에는 마지막에 `Ctrl+L` 을 보내 "전부 다시 그려라" 를 시켰는데,
437
+ Claude Code 는 그걸 **화면 지우기**로 받아 대화가 통째로 스크롤백으로 밀려났다
438
+ (2026-09-11 채증: 정상 화면을 복구했더니 78행 중 0~72행이 비고 입력창만 남았다).
439
+ 그래서 `repairSendRedrawKey` 는 기본으로 꺼져 있다 — 실제로 고치는 것은 잔상 지우기와 크기 정합이라
440
+ 없어도 복구된다. 스크롤백까지 비우는 `repairHard: true` 도 기본은 꺼져 있다.
332
441
 
333
442
  ## 설정
334
443
 
@@ -345,8 +454,6 @@ statusline 이 사라지거나 입력창 구분선이 화면 중간에서 끊기
345
454
  | `sidebarHeight` | `200` | 상/하 도킹 시 높이(px) |
346
455
  | `sidebarMin` / `sidebarMax` | `200` / `560` | 자동 계산 폭의 범위(px). 넓은 모니터에서 정확히 4:3 을 원하면 `sidebarMax` 를 `창 너비 − 창 높이 × 4/3` 으로 |
347
456
  | `sortByStatus` | `false` | 급한 순서(승인대기 → 오류 → 한도 → 진행중 → 완료 → 대기)로 정렬한다. 켜면 **드래그 재정렬은 막힌다** — 옮겨도 다음 렌더에 제자리로 돌아가기 때문 |
348
- | `groupByProject` | `true` | 탭을 작업 폴더별 그룹으로 묶는다. 그룹이 **하나뿐이면 헤더도 그리지 않고 순서도 바꾸지 않는다** |
349
- | `groupRoot` | `''` | 프로젝트 경계 기준 경로. 그 아래 첫 폴더를 한 그룹으로 접는다. 비우면 `rootProfileCwd` 를 쓴다 |
350
457
  | `collapsedGroups` | `[]` | 접어 둔 그룹 키. 헤더를 누르면 여기 쌓인다. 설정 창의 `모두 펼치기` 로 비운다 |
351
458
  | `viewerOpen` | `false` | 미리보기 패널이 열려 있나. `▤` / `agentdeck-view` 로 토글하거나 **새 파일을 주워 저절로 열리면** 여기 남아 다음 기동에 복원된다. 열림을 사람이 켜 두는 값이 아니다 |
352
459
  | `viewerDock` | `right` | 패널이 붙는 방향 `left` / `right`. 사이드바와 같은 쪽이면 사이드바가 창 가장자리(바깥) |
@@ -391,7 +498,7 @@ statusline 이 사라지거나 입력창 구분선이 화면 중간에서 끊기
391
498
  | `autoRepairCooldownMs` | `10000` | 자동 복구 후 이만큼은 다시 하지 않는다 |
392
499
  | `autoRepairMaxPerTab` | `3` | 한 탭에서 자동 복구 최대 횟수. 0 = 무제한 |
393
500
  | `repairHard` | `false` | 화면 복구가 `xterm.reset()` 까지 한다 (스크롤백도 지워진다) |
394
- | `repairSendRedrawKey` | `true` | 화면 복구 마지막에 `Ctrl+L` 을 보내 전체 재렌더를 시킨다 |
501
+ | `repairSendRedrawKey` | `false` | 화면 복구 마지막에 `Ctrl+L` 을 보낸다. **끄는 것이 기본** — Claude Code 는 이걸 재렌더가 아니라 **화면 지우기**로 받아 보고 있던 대화를 스크롤백으로 밀어낸다 (2026-09-11 채증). 켜면 자동 복구에도 적용된다 |
395
502
  | `rootProfile` | `false` | 작업 루트 프로필을 만들어 기본 프로필로 삼는다 (설정 창에서 켠다) |
396
503
  | `rootProfileName` | `Agent Root` | 그 프로필 이름 |
397
504
  | `rootProfileCwd` | `''` | 새 탭이 열릴 경로. 비우면 프로필을 만들지 않는다. 슬래시로 적는다 (예: `D:/Project`) |
package/dist/config.d.ts CHANGED
@@ -134,6 +134,23 @@ export declare class AgentDeckConfigProvider extends ConfigProvider {
134
134
  * 설정 창에서 토글한다 (recovery.service.ts).
135
135
  */
136
136
  noTabRecovery: boolean;
137
+ /**
138
+ * 기동할 때 npm 레지스트리를 보고 **새 버전을 받아 둔다** (적용은 다음 기동).
139
+ *
140
+ * Tabby 는 플러그인을 기동할 때 한 번 읽으므로 돌고 있는 것을 갈아끼울 수 없다 —
141
+ * 그래서 받아만 두고 한 번 알린다. 기동 20초 뒤에 한 번, 그 뒤로는 아래 간격마다.
142
+ *
143
+ * **개발 설치(소스 트리 junction)에는 설치하지 않는다.** 거기에 `npm install` 이 돌면
144
+ * 작업 중인 소스를 덮어쓴다 — 새 버전이 있다는 것만 알리고 손대지 않는다
145
+ * (판정은 `update.ts` 의 `canSelfUpdate`).
146
+ */
147
+ autoUpdate: boolean;
148
+ /** 확인 간격 (시간). 기동마다 묻지 않는다 */
149
+ updateCheckIntervalHours: number;
150
+ /** 마지막으로 레지스트리에 물어본 시각 (epoch ms) — 설정이 아니라 상태다 */
151
+ lastUpdateCheck: number;
152
+ /** 레지스트리 주소. 사내 미러를 쓰면 여기를 바꾼다 */
153
+ updateRegistry: string;
137
154
  /** PTY 출력이 이 시간(ms) 이상 없으면 running -> idle */
138
155
  idleAfterMs: number;
139
156
  /** 출력 패턴으로 상태를 자동 추론할지 */
@@ -240,35 +257,32 @@ export declare class AgentDeckConfigProvider extends ConfigProvider {
240
257
  */
241
258
  dragAutoScroll: boolean;
242
259
  /**
243
- * 사이드바 탭을 **작업 폴더(프로젝트) 단위로 묶고** 그룹 헤더로 접을 수 있게 한다.
260
+ * 접어 둔 그룹 키 목록. 그룹 헤더를 클릭할 때마다 갱신되고 다음 기동에 복원된다.
244
261
  *
245
- * 기본으로 켠다 — 그룹이 하나뿐이면 헤더를 아예 그리지 않으므로(deck.service `render`)
246
- * 한 프로젝트만 쓰는 사람에게는 0.6.0 까지와 화면이 똑같고, 탭이 열 개를 넘겨
247
- * 여러 프로젝트가 섞이는 순간에만 헤더가 생긴다. 즉 켜 둬서 잃는 것이 없다.
248
- * 묶는 기준은 **탭이 태어난 cwd** 다 — 셸에서 `cd` 를 해도 따라오지 않으므로
249
- * (docs/DEVELOPMENT.md 0.5.0 실측) 세션이 사는 동안 그룹이 흔들리지 않는다.
250
- * 그룹 안의 탭 순서는 `sortByStatus` 를 그대로 따른다.
262
+ * 키는 `group.ts` 가 정규화한 경로를 **소문자로 접은** 값이다(`foldGroupKey`) —
263
+ * 셸이 알려주는 경로의 대소문자·구분자가 기동마다 달라도 접힘이 유지돼야 하기 때문이다.
251
264
  */
252
- groupByProject: boolean;
265
+ collapsedGroups: any[];
253
266
  /**
254
- * 프로젝트 경계의 기준 폴더. 비우면 `rootProfileCwd` 를 쓴다.
267
+ * 그룹 안에 **지난 세션**(죽은 Claude Code 세션) 줄을 그린다.
255
268
  *
256
- * 이게 필요한 이유 — cwd 를 그대로 키로 쓰면 `D:\Project\a` 에서 띄운 탭과
257
- * `D:\Project\a\sub` 에서 띄운 탭이 다른 그룹으로 갈린다(서브폴더에서 에이전트를
258
- * 띄우는 건 흔하다). 이 값을 `D:\Project` 로 채워 두면 그 **직하 한 단계**가 그룹이
259
- * 되어 둘이 같은 그룹에 들어온다(`group.ts` `groupKeyOf`).
260
- * 둘 다 비면 cwd 자체가 키다 — 프로젝트들이 한 상위 폴더에 모여 있지 않은 경우다.
261
- * 저장소 루트(`.git`)를 찾아 자동으로 정하지는 않는다 — 렌더는 초당 여러 번 불리므로
262
- * 그때마다 디스크를 훑을 수 없다.
269
+ * 이 기능이 있는 이유는 `recovery.service.ts` 주석에 있다 — agentdeck 은 Tabby 의 순정
270
+ * 탭 복원을 껐다(셸만 살아나고 세션은 죽어 있어 "주인 없는 탭" 이 남는다).
271
+ * `claude --resume` 이 그 자리에 들어갈 진짜 복원이고, 사람이 막히는 곳은 명령이 아니라
272
+ * UUID 목록에서 자기 작업을 찾는 것이라 **라벨을 붙여 보여주는 것** 이 이 기능의 전부다.
263
273
  */
264
- groupRoot: string;
274
+ resumeList: boolean;
275
+ /** 그룹당 몇 줄까지 보여줄지. 0 이면 제한 없음 (목록이 길어지면 살아 있는 탭이 밀린다) */
276
+ resumeListLimit: number;
265
277
  /**
266
- * 접어 둔 그룹 키 목록. 그룹 헤더를 클릭할 때마다 갱신되고 다음 기동에 복원된다.
267
- *
268
- * 키는 `group.ts` 가 정규화한 경로를 **소문자로 접은** 값이다(`foldGroupKey`) —
269
- * 셸이 알려주는 경로의 대소문자·구분자가 기동마다 달라도 접힘이 유지돼야 하기 때문이다.
278
+ * 며칠치까지. 기간 밖 세션은 **화면에서만** 빠지고 원장·기록은 그대로다.
279
+ * stat 단계에서 잘라내므로 이 값이 작을수록 펼치는 순간이 싸다.
270
280
  */
271
- collapsedGroups: any[];
281
+ resumeListDays: number;
282
+ /** 지난 세션 줄을 펼쳐 둔 그룹 키 (`collapsedGroups` 와 반대로 **펼침**을 적는다 — 기본이 접힘이라서) */
283
+ resumeExpanded: any[];
284
+ /** 사람이 목록에서 숨긴 세션 id */
285
+ resumeHidden: any[];
272
286
  /**
273
287
  * 창이 가려져 있어도 사이드바를 계속 갱신한다 (Electron backgroundThrottling 해제).
274
288
  *
@@ -496,8 +510,22 @@ export declare class AgentDeckConfigProvider extends ConfigProvider {
496
510
  */
497
511
  repairHard: boolean;
498
512
  /**
499
- * 복구 마지막에 `Ctrl+L`(0x0C)을 보내 TUI 에게 전체 재렌더를 시킨다.
500
- * 입력 중이던 글자는 유지된다. 이걸 다른 뜻으로 쓰는 앱을 만나면 끈다.
513
+ * 복구 마지막에 `Ctrl+L`(0x0C)을 보낸다.
514
+ *
515
+ * **기본 끔 (2026-09-11 전환).** 0x0C 를 "전부 다시 그려라" 로 받는 앱을 전제했는데,
516
+ * **Claude Code 는 "화면을 지워라" 로 받는다** — 채증으로 확정했다.
517
+ * `↻` 를 누른 뒤 스냅샷(`.agentdeck-screen.log` 06:04:52→54)에서 78행 중 **0~72행이
518
+ * 통째로 비고** 입력창(73~77행)만 남았고, 진단 로그의 순서가 원인을 못 박는다:
519
+ * `clear input area row=73/78`(73~77행만 지움) → `repair redraw-key ctrl-l` → 화면 전멸.
520
+ * 사용자에게는 "새로고침하면 출력이 한참 위로 올라가 버린다" 로 보인다(유저 지적).
521
+ *
522
+ * 끄고도 복구는 된다 — 실제로 고치는 것은 잔상 지우기(`clearInputArea`)와
523
+ * 크기 정합·흔들기(SIGWINCH)이고, 0x0C 는 그 위에 얹은 "확실히 해두기" 였다.
524
+ * 지운 자리의 입력창은 앱이 자기 렌더 주기에 다시 그린다(Claude Code 는 상태줄을
525
+ * 매초 갱신하므로 즉시 돌아온다). 수동 복구는 흔들기까지 하므로 그 자리에서 돌아온다.
526
+ *
527
+ * 켜는 경우 — Ctrl+L 이 실제로 재렌더인 앱만 쓰거나, 셸에서 화면이 지워져도
528
+ * 상관없을 때. 켜면 자동복구에도 적용되므로 오탐 한 번에 보고 있던 화면이 밀린다.
501
529
  */
502
530
  repairSendRedrawKey: boolean;
503
531
  /**
@@ -2,6 +2,7 @@ import { NgZone } from '@angular/core';
2
2
  import { AppService, ConfigService, HostWindowService, HotkeysService, PlatformService, ProfilesService } from 'tabby-core';
3
3
  import { WorkStatusService } from './status.service';
4
4
  import { WorkNotifyService } from './notify.service';
5
+ import { SessionLedgerService } from './sessionLedger.service';
5
6
  /**
6
7
  * 검색어와 후보 필드를 비교 가능한 꼴로 접는다.
7
8
  *
@@ -57,6 +58,7 @@ export declare class AgentDeckService {
57
58
  private hostWindow;
58
59
  private platform;
59
60
  private notify;
61
+ private ledger;
60
62
  private zone;
61
63
  private sidebar;
62
64
  private listEl;
@@ -147,6 +149,11 @@ export declare class AgentDeckService {
147
149
  * `CWD_TTL_MS` 동안 재사용한다 (cd 로 옮겨 다녀도 그 정도면 따라간다).
148
150
  */
149
151
  private cwdCache;
152
+ /**
153
+ * 지난 세션 목록에서 "이미 열림" 으로 그린 줄이 가리키는 탭 — `세션id -> 탭`.
154
+ * 매 렌더마다 `liveSessionIds()` 가 다시 채운다(탭이 닫히면 그 줄은 이어받기로 돌아간다).
155
+ */
156
+ private resumeTabs;
150
157
  /**
151
158
  * 이 서비스의 출력 구독이 실제로 몇 번 돌았나 (진단용).
152
159
  *
@@ -190,7 +197,7 @@ export declare class AgentDeckService {
190
197
  private lastPasteAt;
191
198
  /** 이 시각까지는 PTY 로 나가는 입력을 진단 로그에 남긴다 (붙여넣기가 두 번 들어가는지 보려고) */
192
199
  private pasteProbeUntil;
193
- constructor(app: AppService, config: ConfigService, status: WorkStatusService, hotkeys: HotkeysService, profiles: ProfilesService, hostWindow: HostWindowService, platform: PlatformService, notify: WorkNotifyService, zone: NgZone);
200
+ constructor(app: AppService, config: ConfigService, status: WorkStatusService, hotkeys: HotkeysService, profiles: ProfilesService, hostWindow: HostWindowService, platform: PlatformService, notify: WorkNotifyService, ledger: SessionLedgerService, zone: NgZone);
194
201
  init(): void;
195
202
  /**
196
203
  * 같은 키로는 한 번만 남기는 진단.
@@ -239,10 +246,17 @@ export declare class AgentDeckService {
239
246
  */
240
247
  private repairPane;
241
248
  /**
242
- * TUI 에 "화면을 전부 다시 그려라" 를 보낸다 — `Ctrl+L`(0x0C).
249
+ * `Ctrl+L`(0x0C) 을 보낸다 — **기본 경로가 아니다** (`repairSendRedrawKey`, 기본 끔).
250
+ *
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행을 날린 것은 우리 지우기가 아니라 이 키다.
243
257
  *
244
- * Claude Code·Codex 를 포함해 거의 모든 TUI 와 셸이 이걸 전체 재렌더로 받는다.
245
- * 입력 중이던 글자는 건드리지 않는다(readline·TUI 모두 입력 버퍼는 유지하고 화면만 다시 그린다).
258
+ * 그래서 복구의 기본 수단은 이 키가 아니라 **잔상 지우기 + 크기 정합/흔들기**다.
259
+ * 자세한 근거와 켜는 기준은 `config.ts` 의 `repairSendRedrawKey` 주석에 있다.
246
260
  */
247
261
  private sendRedrawKey;
248
262
  /**
@@ -855,6 +869,16 @@ export declare class AgentDeckService {
855
869
  */
856
870
  private disableBackgroundThrottling;
857
871
  private tick;
872
+ /**
873
+ * 살아 있는 세션의 라벨·cwd·상태를 원장에 적는다 (매초).
874
+ *
875
+ * **여기가 적는 유일한 자리다.** 세 조각이 각각 다른 곳에 있기 때문이다 —
876
+ * 세션↔탭은 `notify`, 라벨·상태는 `status.service`, cwd 는 이 서비스의 `cwdCache`.
877
+ * 한 곳에서 모아 적지 않으면 셋이 어긋난 원장이 남는다.
878
+ *
879
+ * 매초 파일을 쓰지는 않는다 — `note` 가 값이 그대로면 저장을 예약하지 않고, 예약도 2초로 모은다.
880
+ */
881
+ private syncLedger;
858
882
  /**
859
883
  * 사이드바를 **무엇을 어떤 순서로** 그릴지 한 번에 계산한다 (그리지는 않는다).
860
884
  *
@@ -865,6 +889,31 @@ export declare class AgentDeckService {
865
889
  */
866
890
  private renderPlan;
867
891
  private render;
892
+ /**
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;
868
917
  /**
869
918
  * 필터에 걸린 것이 하나도 없을 때의 한 줄.
870
919
  *
@@ -879,6 +928,8 @@ export declare class AgentDeckService {
879
928
  * cwd 는 **이미 채워져 있는 `cwdCache`** 만 본다 — 여기서 `getWorkingDirectory()` 를
880
929
  * 새로 부르면 렌더(초당 여러 번, `scheduleRender`)마다 OS 호출이 탭 수만큼 늘어난다.
881
930
  * 아직 캐시가 빈 탭은 `기타` 에 있다가, 출력이 흐를 때 `touchCwd` 가 채우면 제 그룹으로 옮겨온다.
931
+ * 프로젝트 루트(`projectRootOf`)도 같은 성질이다 — 처음 보는 폴더는 그 순간엔 cwd 자체가
932
+ * 키였다가, 탐지가 끝나면 저장소 루트로 합쳐진다.
882
933
  *
883
934
  * 비용은 탭 수만큼의 맵 조작 + 그룹 수²의 라벨 대조뿐이라(group.ts `groupTabs` 주석)
884
935
  * 캐시를 두지 않는다 — 캐시 무효화용 시그니처를 만드는 비용이 계산 비용과 같기 때문이다.
@@ -1182,6 +1233,38 @@ export declare class AgentDeckService {
1182
1233
  private noteReorderBlocked;
1183
1234
  /** 더블클릭 -> 작업 이름 인라인 편집 */
1184
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;
1185
1268
  /** 우클릭 -> 상태 직접 지정 */
1186
1269
  private showStatusMenu;
1187
1270
  private formatElapsed;
package/dist/group.d.ts CHANGED
@@ -7,25 +7,21 @@
7
7
  * 따라오지 않아 사실상 "탭이 태어난 폴더" 를 준다(docs/DEVELOPMENT.md 0.5.0 실측).
8
8
  * 즉 이 값은 세션이 사는 동안 안 바뀌는 = 그룹 키로 쓸 만한 유일한 값이다.
9
9
  *
10
- * 이 모듈은 **git 저장소 루트를 모른다** (fs 접근이 없다). 프로젝트 경계는 호출부가 넘기는
11
- * `root`(설정 `groupRoot` / `rootProfileCwd`) 로만 정한다 — `groupKeyOf` 주석 참고.
10
+ * 이 모듈은 **여전히 fs 를 모른다.** 프로젝트 경계(저장소 루트)를 실제로 찾는 것은
11
+ * `project-root.ts` 이고, 여기는 호출부가 넘긴 `rootOf` 조회 함수를 부를 뿐이다 —
12
12
  * 경계를 알아내려고 여기서 `.git` 을 찾기 시작하면 렌더마다 디스크를 때리게 된다
13
13
  * (렌더는 초당 여러 번 불린다 — deck.service `scheduleRender`).
14
14
  */
15
15
  /** cwd 를 모르는 탭들이 모이는 그룹의 이름 (Welcome 탭·조회 실패) */
16
16
  export declare const UNGROUPED_LABEL = "\uAE30\uD0C0";
17
- export interface GroupKeyOptions {
18
- /**
19
- * 프로젝트들이 담겨 있는 상위 폴더. cwd 가 이 아래면 **직하 한 단계**까지만 키로 삼는다.
20
- * 없거나 cwd 가 이 밖이면 cwd 자체가 키다.
21
- */
22
- root?: string | null;
23
- /**
24
- * `root` 가 안 걸릴 때 키가 가질 최대 경로 조각 수 (드라이브·UNC 접두는 제외).
25
- * 0 이거나 안 주면 무제한 = cwd 그대로. `root` 아래일 때는 이미 한 단계로 줄었으므로 적용하지 않는다.
26
- */
27
- maxDepth?: number;
28
- }
17
+ /**
18
+ * 정규화된 cwd 를 받아 그 폴더가 속한 **프로젝트 루트**를 돌려주는 조회 함수.
19
+ *
20
+ * 구현은 `project-root.ts` 가 한다 (`.git` 등을 위로 훑는다). 여기서 주입받는 이유는 둘 —
21
+ * 이 모듈을 fs 없이 단위 테스트하기 위해서, 그리고 조회가 캐시·비동기라는 사정을
22
+ * 그룹 규칙이 몰라도 되게 하기 위해서다. 아직 모르면 `null` 을 주면 된다(= cwd 자체가 키).
23
+ */
24
+ export type ProjectRootOf = (cwd: string) => string | null | undefined;
29
25
  export interface GroupedTab<T> {
30
26
  tab: T;
31
27
  key: string | null;
@@ -55,13 +51,23 @@ export declare function foldGroupKey(key: string | null | undefined): string;
55
51
  /**
56
52
  * cwd 를 그룹 키로 — 없으면 null(그룹 없음).
57
53
  *
58
- * 깊이 규칙이 이 함수의 핵심이다. cwd 를 그대로 쓰면 `D:\Project\a\sub` 에서 띄운 탭과
59
- * `D:\Project\a` 에서 띄운 탭이 다른 그룹으로 갈린다(에이전트를 서브폴더에서 띄우는 건 흔하다).
60
- * 그래서 `root` 를 받으면 그 **직하 한 단계**를 키로 삼는다 — `root = D:/Project` 면
61
- * `D:/Project/a/sub` 도 `D:/Project/a` 도 키가 `D:/Project/a` 다.
62
- * `root` 가 없거나 cwd 가 그 밖이면 cwd 자체를 키로 쓴다(그때는 `maxDepth` 로만 줄인다).
54
+ * cwd 를 그대로 키로 쓰면 `D:\Project\a\sub` 에서 띄운 탭과 `D:\Project\a` 에서 띄운 탭이
55
+ * 다른 그룹으로 갈린다(에이전트를 서브폴더에서 띄우는 건 흔하다). 그래서 **그 폴더가 속한
56
+ * 프로젝트 루트**를 키로 삼는다 — 루트는 `rootOf` 가 알려주고(보통 `.git` 이 있는 폴더),
57
+ * 모르면 cwd 자체가 키다(예전 동작 그대로).
58
+ *
59
+ * `rootOf` 가 준 값이 cwd 의 조상이 아니면 **버린다.** 엉뚱한 값 하나가 상관없는 탭들을
60
+ * 한 그룹으로 빨아들이는 것이 "안 묶임" 보다 훨씬 나쁘기 때문이다.
61
+ */
62
+ export declare function groupKeyOf(cwd: string | null | undefined, rootOf?: ProjectRootOf): string | null;
63
+ /**
64
+ * 한 단계 위 폴더 — 더 올라갈 데가 없으면 null.
65
+ *
66
+ * `project-root.ts` 가 마커를 찾아 올라갈 때 쓴다. 여기 두는 이유는 드라이브(`C:/`)·UNC
67
+ * (`//server/share`) 경계를 아는 것이 `splitPath` 뿐이라서다 — `path.dirname` 은 UNC 공유
68
+ * 루트에서 호스트만 남기는 등 경로가 아닌 값을 만든다.
63
69
  */
64
- export declare function groupKeyOf(cwd: string | null | undefined, opts?: GroupKeyOptions): string | null;
70
+ export declare function parentPath(p: string | null | undefined): string | null;
65
71
  /**
66
72
  * 그룹에 붙일 짧은 이름 — 경로 마지막 조각. 다른 그룹과 겹치면 상위를 한 단계씩 더 붙인다.
67
73
  *
@@ -82,7 +88,7 @@ export declare function groupLabelOf(key: string, allKeys: readonly string[]): s
82
88
  * 반환 길이가 1 이면 호출부는 그룹 헤더를 생략한다 — 단일 프로젝트에서 세로 공간을 낭비하지 않게.
83
89
  * 비용은 탭 수만큼의 맵 조작 + 그룹 수²의 라벨 대조뿐이라 렌더마다 불러도 된다.
84
90
  */
85
- export declare function groupTabs<T>(tabs: readonly T[], cwdOf: (t: T) => string | null, opts?: GroupKeyOptions): TabGroup<T>[];
91
+ export declare function groupTabs<T>(tabs: readonly T[], cwdOf: (t: T) => string | null, rootOf?: ProjectRootOf): TabGroup<T>[];
86
92
  /**
87
93
  * 접힘 상태를 설정(`collapsedGroups`)에 적을 때 쓰는 id.
88
94
  *
package/dist/index.d.ts CHANGED
@@ -3,8 +3,9 @@ import { AgentDeckProfileService } from './profile.service';
3
3
  import { WorkNotifyService } from './notify.service';
4
4
  import { AgentDeckAlertService } from './alert.service';
5
5
  import { AgentDeckRecoveryService } from './recovery.service';
6
+ import { AgentDeckUpdateService } from './update.service';
6
7
  import './styles.scss';
7
8
  export * from './api';
8
9
  export default class AgentDeckModule {
9
- constructor(deck: AgentDeckService, profiles: AgentDeckProfileService, notify: WorkNotifyService, alert: AgentDeckAlertService, recovery: AgentDeckRecoveryService);
10
+ constructor(deck: AgentDeckService, profiles: AgentDeckProfileService, notify: WorkNotifyService, alert: AgentDeckAlertService, recovery: AgentDeckRecoveryService, update: AgentDeckUpdateService);
10
11
  }