tabby-agentdeck 0.2.0 → 0.2.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.
@@ -50,6 +50,15 @@ export declare class WorkNotifyService {
50
50
  private timer;
51
51
  /** 폴링을 시작한 시각 — 이보다 오래된 파일은 지난 실행의 잔재다 */
52
52
  private startedAt;
53
+ /**
54
+ * 탭(루트) -> 그 안 패널들의 셸 PID. 훅이 보낸 조상 PID 와 맞춰 어느 탭의 보고인지 확정한다.
55
+ * pty 에 물어보는 비동기 작업이라 표로 들고 있고, 탭이 열릴 때와 2초마다 새로 채운다
56
+ */
57
+ private tabPids;
58
+ private pidRefreshing;
59
+ private pollCount;
60
+ /** 같은 매칭 실패를 폴링마다 diag 에 찍지 않기 위한 마지막 메시지 */
61
+ private lastBindLog;
53
62
  constructor(app: AppService, config: ConfigService, status: WorkStatusService, platform: PlatformService);
54
63
  /**
55
64
  * 훅이 안 걸려 있으면 기동할 때 한 번 물어본다.
@@ -60,6 +69,14 @@ export declare class WorkNotifyService {
60
69
  */
61
70
  private promptHooksOnce;
62
71
  init(): void;
72
+ /** 진단 한 줄 — deck.service 와 같은 파일(`~/.agentdeck-diag.log`)에 남긴다 */
73
+ private diag;
74
+ private tabName;
75
+ /**
76
+ * 탭별 셸 PID 표를 새로 채운다. 로컬 pty 는 `session.pty.getPID()` 로 셸 PID 를 준다
77
+ * (tabby-local Session — gracefullyKillProcess 가 쓰는 그 경로). SSH 등 PID 가 없는 탭은 빈 목록.
78
+ */
79
+ private refreshPids;
63
80
  /** 진단용 */
64
81
  get debugDir(): string;
65
82
  /** 진단용 — 듣고 있는 포트 (0 = TCP 채널이 안 떴다) */
@@ -91,12 +108,24 @@ export declare class WorkNotifyService {
91
108
  */
92
109
  private apply;
93
110
  /**
94
- * 세션에 묶인 탭을 찾는다. 처음 보는 세션이면 지금 활성 탭에 묶는다.
111
+ * 세션에 묶인 탭을 찾는다.
112
+ *
113
+ * 1순위는 **프로세스 계보**다 — 훅이 보낸 조상 PID(claude.exe → 탭 셸 → Tabby) 에 어떤 탭의
114
+ * 셸 PID 가 들어 있으면 그 탭이다(bind.ts). 이미 다른 세션이 그 탭을 갖고 있어도 넘겨받는다:
115
+ * 같은 탭에서 claude 를 다시 띄우면 세션 id 가 바뀌는 게 정상이고, 이전 주인은 이미 죽은 세션이다.
116
+ * 알던 탭과 다른 탭이 맞으면 옮긴다 (`claude --resume` 을 다른 탭에서 띄운 경우).
117
+ *
118
+ * pids 가 왔는데 맞는 탭이 없으면 **추측하지 않고** null 이다 — 탭 PID 표가 아직 안 채워진 것일 수
119
+ * 있고, 그 경우 다음 폴링(400ms)에 다시 온다. 활성 탭으로 떨어지던 예전 규칙이 낸 사고가
120
+ * "끝난 세션의 done 이 새 탭에 박힘" 이었다 (2026-09-02 실측).
95
121
  *
96
- * 단 그 탭을 이미 다른 세션이 점유했으면 **묶지 않는다**. 예전엔 이 검사가 없어서
97
- * 여러 세션이 한 탭에 겹쳐 붙었고, 서로의 상태를 덮어써 "작업 중인 탭이 완료로,
98
- * 끝난 탭이 진행 중으로" 뒤바뀌어 보였다. 못 묶은 세션은 사용자가 그 탭을 활성화하는
99
- * 순간(= 다음 보고 때) 제 짝을 찾는다.
122
+ * pids 가 없는 보고(옛 훅·셸에서 사람이 부른 것)만 예전 규칙을 탄다 — 처음 보는 세션이면 지금 활성 탭.
123
+ * 단 그 탭을 이미 다른 세션이 점유했으면 묶지 않는다 (여러 세션이 한 탭에 겹쳐 붙어 서로의
124
+ * 상태를 덮어쓰던 2026-08-28 사고의 방지책).
100
125
  */
101
126
  private resolveTab;
127
+ /** 세션 <-> 탭 을 묶고, 그 탭의 이전 주인과 이 세션의 이전 탭은 풀어 준다 */
128
+ private bind;
129
+ /** 매칭 실패는 폴링마다 반복되므로 같은 내용은 한 번만 남긴다 */
130
+ private bindLog;
102
131
  }
@@ -76,6 +76,7 @@ $safeId = ($targetId -replace '[^A-Za-z0-9._-]', '_')
76
76
  $file = Join-Path $dir "$safeId.json"
77
77
 
78
78
  # 직전 상태를 읽어 둔다 — 승인대기 오탐 방지에 쓴다
79
+ $prev = $null
79
80
  $prevStatus = ''
80
81
  if (Test-Path $file) {
81
82
  try {
@@ -84,6 +85,61 @@ if (Test-Path $file) {
84
85
  } catch { }
85
86
  }
86
87
 
88
+ # --- 프로세스 계보(pids) ---
89
+ # 사이드바가 이 보고를 어느 탭에 붙일지 정하는 근거. 훅 프로세스의 조상을 위로 따라가면
90
+ # (훅 pwsh) <- bash <- claude.exe <- 탭 셸(powershell/pwsh) <- Tabby.exe 가 나오고,
91
+ # 플러그인은 탭별 셸 PID 와 맞춰 어느 탭인지 확정한다 (src/bind.ts).
92
+ # 예전 규칙 "처음 보고할 때의 활성 탭" 은 끝난 세션의 done 이 새 탭에 박히는 사고를 냈다 (2026-09-02 실측).
93
+ #
94
+ # 훅 JSON 이 있을 때만 계산한다 — 셸에서 사람이 -SessionId 로 직접 부르면 조상이 탭 셸 자체라
95
+ # 걸러지고 Tabby.exe 만 남아 어느 탭에도 안 맞는다. 그 경로는 예전 활성 탭 규칙에 맡긴다.
96
+ #
97
+ # 비용 — Win32_Process 전체 조회가 ~450ms 라 세션당 한 번만 하고 status 파일에 캐시한다
98
+ # (홉마다 필터 조회하면 ~2초로 더 느리다). 캐시는 claude 프로세스(claudePid)가 살아 있고
99
+ # 이름이 같을 때만 재사용한다 — `claude --resume` 을 다른 탭에서 띄우면 PID 가 바뀌어 다시 계산된다.
100
+ $pids = @()
101
+ $claudePid = 0
102
+ $claudeName = ''
103
+ if ($hook) {
104
+ $reuse = $false
105
+ if ($prev -and $prev.pids -and $prev.claudePid) {
106
+ $aliveProc = Get-Process -Id ([int]$prev.claudePid) -ErrorAction SilentlyContinue
107
+ if ($aliveProc -and (("$($aliveProc.ProcessName).exe") -ieq [string]$prev.claudeName)) {
108
+ $pids = @($prev.pids | ForEach-Object { [int]$_ })
109
+ $claudePid = [int]$prev.claudePid
110
+ $claudeName = [string]$prev.claudeName
111
+ $reuse = $true
112
+ }
113
+ }
114
+ if (-not $reuse) {
115
+ # 훅 자신과 그것을 띄운 셸 껍데기 — 매번 바뀌는 일회성 프로세스라 맞춰 볼 대상이 아니다.
116
+ # 앞에서부터 이 이름들을 건너뛰고 처음 만나는 것이 에이전트 프로세스(claude.exe / node.exe)다.
117
+ # 그 뒤의 탭 셸(powershell.exe)은 접두만 건너뛰므로 남는다
118
+ $transient = @('powershell.exe', 'pwsh.exe', 'bash.exe', 'sh.exe', 'cmd.exe', 'conhost.exe')
119
+ try {
120
+ $all = @{}
121
+ Get-CimInstance Win32_Process -Property ProcessId, ParentProcessId, Name -ErrorAction Stop |
122
+ ForEach-Object { $all[[int]$_.ProcessId] = $_ }
123
+ $chain = @()
124
+ $cur = [int]$PID
125
+ for ($i = 0; $i -lt 16 -and $cur -gt 0 -and $all.ContainsKey($cur); $i++) {
126
+ $row = $all[$cur]
127
+ $chain += $row
128
+ $next = [int]$row.ParentProcessId
129
+ if ($next -eq $cur) { break }
130
+ $cur = $next
131
+ }
132
+ $start = 0
133
+ while ($start -lt $chain.Count -and ($transient -contains ([string]$chain[$start].Name).ToLower())) { $start++ }
134
+ if ($start -lt $chain.Count) {
135
+ $claudePid = [int]$chain[$start].ProcessId
136
+ $claudeName = [string]$chain[$start].Name
137
+ $pids = @($chain[$start..($chain.Count - 1)] | ForEach-Object { [int]$_.ProcessId })
138
+ }
139
+ } catch { }
140
+ }
141
+ }
142
+
87
143
  # --- 승인대기 오탐 차단 ---
88
144
  # Notification 훅은 "도구 승인 요청" 말고 "입력이 없다"는 유휴 알림에도 발화한다.
89
145
  # 그래서 Stop(done) 뒤에 유휴 알림이 오면 완료된 탭이 '승인대기' 로 뒤집힌다 (2026-08-28 실측:
@@ -121,6 +177,11 @@ $payload = [ordered]@{
121
177
  ts = [int64](([datetime]::UtcNow - [datetime]'1970-01-01').TotalMilliseconds)
122
178
  }
123
179
  if ($newLabel) { $payload['label'] = $newLabel }
180
+ if ($pids.Count -gt 0) {
181
+ $payload['pids'] = [int[]]$pids
182
+ $payload['claudePid'] = $claudePid
183
+ $payload['claudeName'] = $claudeName
184
+ }
124
185
 
125
186
  # BOM 없는 UTF-8 로 원자적 쓰기 — Node 의 JSON.parse 는 BOM 을 못 먹는다
126
187
  #
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tabby-agentdeck",
3
- "version": "0.2.0",
3
+ "version": "0.2.1",
4
4
  "description": "Run AI coding agents side by side in Tabby - a live deck of agent sessions with status, agent-friendly hotkeys and image paste",
5
5
  "keywords": [
6
6
  "tabby-plugin",
@@ -16,7 +16,7 @@
16
16
  "scripts": {
17
17
  "build": "webpack --progress --color",
18
18
  "watch": "webpack --progress --color --watch",
19
- "test": "tsc src/prompt.ts src/detect.ts src/claudeHooks.ts src/screen.ts --outDir .tmp --module commonjs --target es2019 --skipLibCheck --experimentalDecorators --esModuleInterop && node test/prompt.test.js && node test/detect.test.js && node test/claudeHooks.test.js && node test/screen.test.js"
19
+ "test": "tsc src/prompt.ts src/detect.ts src/claudeHooks.ts src/screen.ts src/bind.ts --outDir .tmp --module commonjs --target es2019 --skipLibCheck --experimentalDecorators --esModuleInterop && node test/prompt.test.js && node test/detect.test.js && node test/claudeHooks.test.js && node test/screen.test.js && node test/bind.test.js"
20
20
  },
21
21
  "files": [
22
22
  "dist",