tabby-agentdeck 0.2.0 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +193 -178
- package/dist/alert.service.d.ts +87 -0
- package/dist/api.d.ts +16 -2
- package/dist/bind.d.ts +23 -0
- package/dist/claudeHooks.d.ts +2 -1
- package/dist/config.d.ts +47 -0
- package/dist/deck.service.d.ts +58 -0
- package/dist/index.d.ts +3 -1
- package/dist/index.js +1403 -62
- package/dist/index.js.map +1 -1
- package/dist/notify.service.d.ts +92 -5
- package/dist/order.d.ts +26 -0
- package/dist/reason.d.ts +16 -0
- package/dist/recovery.service.d.ts +32 -0
- package/dist/settings.component.d.ts +4 -2
- package/dist/status.service.d.ts +14 -2
- package/dist/tabenv.d.ts +26 -0
- package/hooks/agentdeck-notify.ps1 +126 -6
- package/hooks/agentdeck-status.ps1 +2 -2
- package/install.ps1 +2 -31
- package/package.json +2 -2
package/dist/notify.service.d.ts
CHANGED
|
@@ -50,6 +50,25 @@ 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
|
+
/**
|
|
60
|
+
* 탭(루트) -> 그 안 패널 셸에 심은 `AGENTDECK_TAB` 값들(tabenv.ts). 훅이 tabId 로 보내면 여기서 바로
|
|
61
|
+
* 확정하고, 프로세스 계보(tabPids)는 이 표에 없을 때의 폴백이다. 패널의 `profile.options.env` 를 읽어
|
|
62
|
+
* 다시 채우므로 재시작으로 복원된 탭(복원 토큰에 profile 이 통째로 들어간다, tabby-local
|
|
63
|
+
* dist/index.js:639-648)도 지난 실행의 id 를 그대로 되찾는다 — 살아 있는 pty 에 다시 붙든, 복원이
|
|
64
|
+
* 실패해 같은 options 로 새 셸을 띄우든 셸 환경은 그 값이다
|
|
65
|
+
*/
|
|
66
|
+
private tabIds;
|
|
67
|
+
/** tabAdded$ 를 이미 구독한 분할 루트 — 루트가 사라지면 함께 사라진다 */
|
|
68
|
+
private watchedSplits;
|
|
69
|
+
private pollCount;
|
|
70
|
+
/** 같은 매칭 실패를 폴링마다 diag 에 찍지 않기 위한 마지막 메시지 */
|
|
71
|
+
private lastBindLog;
|
|
53
72
|
constructor(app: AppService, config: ConfigService, status: WorkStatusService, platform: PlatformService);
|
|
54
73
|
/**
|
|
55
74
|
* 훅이 안 걸려 있으면 기동할 때 한 번 물어본다.
|
|
@@ -60,6 +79,60 @@ export declare class WorkNotifyService {
|
|
|
60
79
|
*/
|
|
61
80
|
private promptHooksOnce;
|
|
62
81
|
init(): void;
|
|
82
|
+
/** 진단 한 줄 — deck.service 와 같은 파일(`~/.agentdeck-diag.log`)에 남긴다 */
|
|
83
|
+
private diag;
|
|
84
|
+
private tabName;
|
|
85
|
+
/**
|
|
86
|
+
* 탭별 셸 PID 표를 새로 채운다. 로컬 pty 는 `session.pty.getPID()` 로 셸 PID 를 준다
|
|
87
|
+
* (tabby-local Session — gracefullyKillProcess 가 쓰는 그 경로). SSH 등 PID 가 없는 탭은 빈 목록.
|
|
88
|
+
*/
|
|
89
|
+
private refreshPids;
|
|
90
|
+
/** 루트 하나의 터미널 패널들 — 분할 래퍼(`app.tabs` 의 원소는 SplitTabComponent)면 자식들, 아니면 자기 자신 */
|
|
91
|
+
private panesOf;
|
|
92
|
+
private isLocalProfile;
|
|
93
|
+
/**
|
|
94
|
+
* 새 루트 탭의 셸에 `AGENTDECK_TAB` 을 심는다 — 지금 있는 패널, 복원 토큰 안의 패널, 나중에 분할로 붙는 패널.
|
|
95
|
+
*
|
|
96
|
+
* 심는 자리가 맞는 근거 (tabby-local/dist/index.js, 1.0.231):
|
|
97
|
+
* - 셸은 `onFrontendReady -> initializeSession` 이 `session.start({ ...this.profile.options, width, height })`
|
|
98
|
+
* 로 띄운다(623-635). 자식 env 는 `mergeEnv(process.env, {TERM_PROGRAM: 'Tabby', …}, substituteEnv(options.env),
|
|
99
|
+
* config.store.terminal.environment)` 다(1158-1162). 즉 **spawn 시점의 `pane.profile.options.env`** 가 셸 환경이다.
|
|
100
|
+
* - `ngOnInit` 의 `this.sessionOptions = this.profile.options`(604-605) 는 같은 객체를 가리키는 참조일 뿐
|
|
101
|
+
* spawn 에 쓰이지 않는다. profile 을 새 객체로 바꾸므로 그것도 같이 맞춰 둔다.
|
|
102
|
+
* - `tab.profile` 은 `getNewTabParameters` 가 deep clone 한 것(898)이라 저장된 프로필과 다른 객체다. 그래도
|
|
103
|
+
* 여기서 다시 얕게 복제해 넣는다 — 다른 경로(복원·복제 토큰)로 들어온 profile 이 공유 객체일 가능성을 막고,
|
|
104
|
+
* 탭 하나의 id 가 설정 파일의 프로필로 새는 일이 없게 한다.
|
|
105
|
+
*
|
|
106
|
+
* 타이밍 (tabby-core/dist/index.js): `openNewTab`(5075) 은 `wrapAndAddTab`(5086) 에서 `splitTab.addTab(tab)` 을
|
|
107
|
+
* 부르고 — `add()` 는 첫 await(2620 `initialized$`) 앞에서 자식을 `children` 에 넣는다(2618) — 그 다음
|
|
108
|
+
* `addTabRaw` 가 `tabOpened.next(splitTab)` 를 낸다(5029). 그래서 tabOpened 시점에 `getAllTabs()` 에 자식이
|
|
109
|
+
* 있고 `session` 은 아직 null 이다.
|
|
110
|
+
*/
|
|
111
|
+
private stampRoot;
|
|
112
|
+
private stampPanes;
|
|
113
|
+
/**
|
|
114
|
+
* 패널 하나에 심는다. **아직 셸이 안 뜬**(`session` 없음) 로컬 패널만 — 이미 뜬 셸의 환경은 바꿀 수 없다.
|
|
115
|
+
* 복원 패널(`restoreFromPTYID`)은 살아 있는 pty 에 다시 붙으므로 건드리지 않는다 — 토큰의 profile 에
|
|
116
|
+
* 지난 실행의 id 가 있으면 그게 곧 그 셸의 환경이고 refreshTabIds 가 읽어 간다.
|
|
117
|
+
* 이미 id 가 있어도 다른 패널이 같은 값을 갖고 있으면(복제) 새로 뽑는다 — 같은 id 가 두 셸에 있으면 어느 쪽인지 알 수 없다.
|
|
118
|
+
*/
|
|
119
|
+
private stampPane;
|
|
120
|
+
/**
|
|
121
|
+
* 복원 토큰 트리(`app:split-tab` 은 children, 잎은 profile 을 가진다)의 로컬 패널에 심는다.
|
|
122
|
+
* `restoreFromPTYID` 가 있는 잎은 재시작 복원(saveTabs 가 includeState 로 만든 토큰, tabby-core 7700) —
|
|
123
|
+
* 살아 있는 pty 의 환경은 이미 정해져 있으니 두고, 없는 잎은 새 셸이 뜰 것이므로(복제 탭 —
|
|
124
|
+
* `tabsService.duplicate` 는 options 없이 토큰을 만들어 restoreFromPTYID 가 비어 있다, 7819-7830) 항상 새 id 를 준다.
|
|
125
|
+
* 복제 원본의 id 가 토큰에 그대로 실려 오는데 그걸 두면 두 탭이 같은 id 를 갖게 된다
|
|
126
|
+
*/
|
|
127
|
+
private stampRecoveryState;
|
|
128
|
+
/** 같은 id 를 다른 패널(어느 루트든)이 이미 갖고 있나 */
|
|
129
|
+
private idTakenElsewhere;
|
|
130
|
+
/**
|
|
131
|
+
* 탭별 tabId 표를 패널의 `profile.options.env` 에서 다시 읽는다. 동기·저비용이라 폴링(400ms)마다 돈다 —
|
|
132
|
+
* 복원 탭의 자식은 tabOpened 뒤에 비동기로 생기고(tabby-core 3093) 이벤트도 없어서 읽는 쪽이 주기적으로 봐야 한다.
|
|
133
|
+
* 같은 id 가 두 루트에 있으면(복제 뒤 셸이 이미 뜬 경우) 어느 쪽인지 알 수 없으니 둘 다 표에서 빼고 계보 폴백에 맡긴다.
|
|
134
|
+
*/
|
|
135
|
+
private refreshTabIds;
|
|
63
136
|
/** 진단용 */
|
|
64
137
|
get debugDir(): string;
|
|
65
138
|
/** 진단용 — 듣고 있는 포트 (0 = TCP 채널이 안 떴다) */
|
|
@@ -91,12 +164,26 @@ export declare class WorkNotifyService {
|
|
|
91
164
|
*/
|
|
92
165
|
private apply;
|
|
93
166
|
/**
|
|
94
|
-
* 세션에 묶인 탭을 찾는다.
|
|
167
|
+
* 세션에 묶인 탭을 찾는다.
|
|
168
|
+
*
|
|
169
|
+
* 1순위는 **셸에 심어 둔 `AGENTDECK_TAB`**(tabId, tabenv.ts) — 플러그인이 탭을 열 때 심은 값을 훅이 그대로
|
|
170
|
+
* 돌려보낸 것이라 표에 있으면 곧 그 탭이다. 2순위는 **프로세스 계보**(pids) — 훅이 보낸 조상 PID(claude.exe →
|
|
171
|
+
* 탭 셸 → Tabby) 에 어떤 탭의 셸 PID 가 들어 있으면 그 탭이다(bind.ts). 이 버전 이전에 열린 탭·복원된 탭이
|
|
172
|
+
* 여기로 온다. 둘 다 이미 다른 세션이 그 탭을 갖고 있어도 넘겨받는다: 같은 탭에서 claude 를 다시 띄우면
|
|
173
|
+
* 세션 id 가 바뀌는 게 정상이고, 이전 주인은 이미 죽은 세션이다. 알던 탭과 다른 탭이 맞으면 옮긴다
|
|
174
|
+
* (`claude --resume` 을 다른 탭에서 띄운 경우).
|
|
175
|
+
*
|
|
176
|
+
* tabId 나 pids 가 왔는데 맞는 탭이 없으면 **추측하지 않고** null 이다 — 표가 아직 안 채워진 것일 수
|
|
177
|
+
* 있고(복원 탭의 자식은 늦게 생긴다), 그 경우 다음 폴링(400ms)에 다시 온다. 활성 탭으로 떨어지던 예전 규칙이
|
|
178
|
+
* 낸 사고가 "끝난 세션의 done 이 새 탭에 박힘" 이었다 (2026-09-02 실측).
|
|
95
179
|
*
|
|
96
|
-
*
|
|
97
|
-
* 여러 세션이 한 탭에 겹쳐
|
|
98
|
-
*
|
|
99
|
-
* 순간(= 다음 보고 때) 제 짝을 찾는다.
|
|
180
|
+
* 둘 다 없는 보고(옛 훅·셸에서 사람이 부른 것)만 예전 규칙을 탄다 — 처음 보는 세션이면 지금 활성 탭.
|
|
181
|
+
* 단 그 탭을 이미 다른 세션이 점유했으면 묶지 않는다 (여러 세션이 한 탭에 겹쳐 붙어 서로의
|
|
182
|
+
* 상태를 덮어쓰던 2026-08-28 사고의 방지책).
|
|
100
183
|
*/
|
|
101
184
|
private resolveTab;
|
|
185
|
+
/** 세션 <-> 탭 을 묶고, 그 탭의 이전 주인과 이 세션의 이전 탭은 풀어 준다 */
|
|
186
|
+
private bind;
|
|
187
|
+
/** 매칭 실패는 폴링마다 반복되므로 같은 내용은 한 번만 남긴다 */
|
|
188
|
+
private bindLog;
|
|
102
189
|
}
|
package/dist/order.d.ts
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 사이드바 정렬·집계의 순수 규칙 — Angular/Tabby 에 기대지 않아 단위 테스트가 된다.
|
|
3
|
+
*
|
|
4
|
+
* 헤더 집계와 `sortByStatus` 정렬이 같은 우선순위를 써야 하므로 한 곳에 둔다.
|
|
5
|
+
* 순서는 "사람이 지금 손대야 하는 것" 부터다 —
|
|
6
|
+
* waiting(승인대기) → error(오류) → limited(한도 도달) → running(진행중) → done(완료) → idle(대기)
|
|
7
|
+
* 한도 도달은 사람이 풀 수 있는 일은 아니지만 "이 세션은 멈춰 있다" 를 알아야 하므로 진행중보다 앞에 둔다.
|
|
8
|
+
* 세션을 여섯 개쯤 띄워 두면 어느 탭이 나를 기다리는지 한눈에 봐야 한다 (2026-09-02 요청).
|
|
9
|
+
*/
|
|
10
|
+
import { WorkStatus } from './api';
|
|
11
|
+
/** 표시·정렬 우선순위. 앞이 더 급하다 */
|
|
12
|
+
export declare const STATUS_ORDER: readonly WorkStatus[];
|
|
13
|
+
/** 우선순위 숫자 — 작을수록 앞. 모르는 값은 맨 뒤로 보낸다 */
|
|
14
|
+
export declare function statusRank(status: WorkStatus | string): number;
|
|
15
|
+
/**
|
|
16
|
+
* 상태별 개수. `STATUS_ORDER` 순서로, 0 인 상태는 빼고 돌려준다 — 헤더에 그대로 그리면 된다.
|
|
17
|
+
*/
|
|
18
|
+
export declare function countByStatus<T>(tabs: readonly T[], statusOf: (tab: T) => WorkStatus): Array<{
|
|
19
|
+
status: WorkStatus;
|
|
20
|
+
count: number;
|
|
21
|
+
}>;
|
|
22
|
+
/**
|
|
23
|
+
* 상태 우선순위로 정렬한 **복사본**. 같은 상태끼리는 원래 탭 순서를 지킨다(안정 정렬).
|
|
24
|
+
* 원본 배열(`app.tabs`)은 건드리지 않는다 — Tabby 의 탭 순서는 사용자의 것이다.
|
|
25
|
+
*/
|
|
26
|
+
export declare function sortTabsByStatus<T>(tabs: readonly T[], statusOf: (tab: T) => WorkStatus): T[];
|
package/dist/reason.d.ts
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 승인대기 이유 줄이기 — Notification 훅의 message 원문을 배지에 붙일 짧은 한국어로 바꾼다.
|
|
3
|
+
*
|
|
4
|
+
* 훅(hooks/agentdeck-notify.ps1)은 원문을 그대로 보내고, 줄이는 규칙은 여기 한 곳에만 둔다.
|
|
5
|
+
* Claude Code 가 문구를 바꾸면 이 파일만 고치면 되고, 훅을 다시 설치할 필요가 없다.
|
|
6
|
+
*
|
|
7
|
+
* 알려진 문구 (2026-09 기준):
|
|
8
|
+
* "Claude needs your permission to use Bash" -> "Bash 권한"
|
|
9
|
+
* "Claude is waiting for your input" -> "입력 대기"
|
|
10
|
+
* 플랜 승인 / 질문 응답도 같은 요령으로 잡는다.
|
|
11
|
+
* 한도 도달(StopFailure error=rate_limit 의 last_assistant_message):
|
|
12
|
+
* "You've hit your session limit · resets 12pm (Asia/Seoul)" -> "12pm 리셋"
|
|
13
|
+
* 리셋 시각이 없으면 빈 값 — 배지가 이미 `⛔ 한도 도달` 이라 같은 말을 되풀이하지 않는다.
|
|
14
|
+
* 모르는 문구는 앞 24자만 남긴다 — 배지 한 줄을 넘기지 않기 위해서다.
|
|
15
|
+
*/
|
|
16
|
+
export declare function shortenReason(msg: string): string;
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { AppService, ConfigService } from 'tabby-core';
|
|
2
|
+
/**
|
|
3
|
+
* "기동 시 탭 복원 끄기" — Tabby 의 `recoverTabs` 를 강제로 false 에 묶는다.
|
|
4
|
+
*
|
|
5
|
+
* 왜 따로 있나: Tabby 는 종료 때 열려 있던 탭을 `localStorage.tabsRecovery` 에 저장하고
|
|
6
|
+
* 다음 기동에서 `config.store.recoverTabs` 가 참이면 되살린다(tabby-core/dist/index.js:5003).
|
|
7
|
+
* 그런데 복원되는 것은 **셸만**이다 — 그 안에서 돌던 Claude Code 세션은 이미 죽어 있어
|
|
8
|
+
* 되살아난 탭은 빈 프롬프트일 뿐이고, 사이드바에는 주인 없는 탭이 줄줄이 남는다.
|
|
9
|
+
* 그래서 사용자가 고르면 "무조건 빈 상태로 시작" 을 보장한다.
|
|
10
|
+
*
|
|
11
|
+
* 두 상태만 있다:
|
|
12
|
+
* - `agentDeck.noTabRecovery = false` : 순정. Tabby 의 `recoverTabs` 값을 건드리지 않는다.
|
|
13
|
+
* - `agentDeck.noTabRecovery = true` : 무조건 끔. Tabby 설정 창에서 `recoverTabs` 를 다시 켜도
|
|
14
|
+
* 기동 때와 설정이 바뀔 때마다 false 로 되돌린다(`enforce`).
|
|
15
|
+
*
|
|
16
|
+
* 기동 분기(5003)는 `config.ready$` 직후라 플러그인보다 먼저 지나간다 — 그래서 값은 토글 순간에
|
|
17
|
+
* config.yaml 에 써 두고, 여기서는 그 뒤로 누가 뒤집어 놓는 경우만 막는다.
|
|
18
|
+
*/
|
|
19
|
+
export declare class AgentDeckRecoveryService {
|
|
20
|
+
private app;
|
|
21
|
+
private config;
|
|
22
|
+
constructor(app: AppService, config: ConfigService);
|
|
23
|
+
init(): void;
|
|
24
|
+
/**
|
|
25
|
+
* 토글 진입점. 켜면 `recoverTabs=false` + 이미 저장된 복원 목록도 지운다 —
|
|
26
|
+
* 남겨 두면 나중에 순정으로 되돌렸을 때 그때의 낡은 탭들이 되살아난다.
|
|
27
|
+
* 끄면 Tabby 기본값(true)으로 되돌린다.
|
|
28
|
+
*/
|
|
29
|
+
setNoTabRecovery(on: boolean): void;
|
|
30
|
+
/** noTabRecovery 가 켜져 있는데 recoverTabs 가 참으로 돌아와 있으면 다시 끈다. 필요할 때만 저장한다 */
|
|
31
|
+
private enforce;
|
|
32
|
+
}
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import { ConfigService, PlatformService } from 'tabby-core';
|
|
2
|
+
import { AgentDeckRecoveryService } from './recovery.service';
|
|
2
3
|
/**
|
|
3
4
|
* 설정 > AgentDeck 탭.
|
|
4
5
|
*
|
|
5
|
-
* 사용자가 실제로 바꿀 일이 있는
|
|
6
|
+
* 사용자가 실제로 바꿀 일이 있는 것만 노출한다 — 레이아웃 on/off, 기동 시 탭 복원 끄기, 작업 루트 프로필.
|
|
6
7
|
* 나머지(화면비, 사이드바 폭, 투명도, 상태 감지, 입력 가로채기, 렌더 보정, 통보 채널)는
|
|
7
8
|
* 손댈 이유가 거의 없고 잘못 만지면 화면이 깨지는 값이라 `config.ts` 의 기본값으로 고정하고
|
|
8
9
|
* 설정 창에서는 뺐다. 정말 필요하면 설정 파일의 `agentDeck.*` 를 직접 고친다.
|
|
@@ -12,6 +13,7 @@ import { ConfigService, PlatformService } from 'tabby-core';
|
|
|
12
13
|
*/
|
|
13
14
|
export declare class AgentDeckSettingsTabComponent {
|
|
14
15
|
config: ConfigService;
|
|
16
|
+
recovery: AgentDeckRecoveryService;
|
|
15
17
|
private platform;
|
|
16
18
|
/** 이 환경에서 훅을 걸 수 있나 (통보 스크립트가 PowerShell 이라 Windows 전용) */
|
|
17
19
|
hooksSupported: boolean;
|
|
@@ -19,7 +21,7 @@ export declare class AgentDeckSettingsTabComponent {
|
|
|
19
21
|
hooksOn: boolean;
|
|
20
22
|
/** 훅을 쓰다 실패한 이유 — 조용히 삼키면 사용자는 버튼이 먹통인 줄 안다 */
|
|
21
23
|
hookError: string;
|
|
22
|
-
constructor(config: ConfigService, platform: PlatformService);
|
|
24
|
+
constructor(config: ConfigService, recovery: AgentDeckRecoveryService, platform: PlatformService);
|
|
23
25
|
private refreshHooks;
|
|
24
26
|
/**
|
|
25
27
|
* 훅을 걸거나 뗀다.
|
package/dist/status.service.d.ts
CHANGED
|
@@ -20,11 +20,23 @@ export declare class WorkStatusService {
|
|
|
20
20
|
get(tab: BaseTabComponent): TabState;
|
|
21
21
|
/** 자동 감지용 — pinned 상태는 건드리지 않는다 */
|
|
22
22
|
setAuto(tab: BaseTabComponent, status: WorkStatus): void;
|
|
23
|
-
/**
|
|
24
|
-
|
|
23
|
+
/**
|
|
24
|
+
* 수동/훅 지정 — 이후 자동 감지보다 우선한다.
|
|
25
|
+
* `reason` 은 상태의 부연(승인 이유 / 한도 리셋 시각 / 오류 코드) — REASON_STATUSES 일 때만 담는다
|
|
26
|
+
*/
|
|
27
|
+
setManual(tab: BaseTabComponent, status: WorkStatus, label?: string, reason?: string): void;
|
|
28
|
+
/**
|
|
29
|
+
* 승인대기 → 진행중. 사람이 승인한 순간을 훅은 알려주지 않는다 — 다음 훅(PostToolUse)은
|
|
30
|
+
* 도구가 **끝난 뒤**라 긴 빌드 동안 사이드바가 승인대기에 박혀 있다. 그래서 화면에 "esc to interrupt"
|
|
31
|
+
* 같은 작업 중 표시가 다시 보이면 여기로 올린다 (detect.ts). pinned·label 은 그대로 둔다 —
|
|
32
|
+
* 훅이 고정한 탭도 이 전이만은 허용해야 하기 때문이다. waiting 이 아니면 아무것도 하지 않는다
|
|
33
|
+
*/
|
|
34
|
+
resume(tab: BaseTabComponent): void;
|
|
25
35
|
setLabel(tab: BaseTabComponent, label: string): void;
|
|
26
36
|
/** 수동 고정을 풀고 자동 감지로 되돌린다 */
|
|
27
37
|
unpin(tab: BaseTabComponent): void;
|
|
38
|
+
/** 부연을 가질 수 없는 상태에는 이유가 남아 있을 수 없다 */
|
|
39
|
+
private clearReasonUnlessWaiting;
|
|
28
40
|
markOutput(tab: BaseTabComponent): void;
|
|
29
41
|
/**
|
|
30
42
|
* 에이전트가 "작업 중" 이라고 스스로 밝힌 순간 — 스피너나 "esc to interrupt" 같은 표시.
|
package/dist/tabenv.d.ts
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 훅 보고를 탭에 직결하는 열쇠 — 탭 셸에 심는 환경변수와 그것으로 탭을 고르는 순수 규칙.
|
|
3
|
+
* Angular/Tabby 에 기대지 않아 단위 테스트가 된다 (test/tabenv.test.js).
|
|
4
|
+
*
|
|
5
|
+
* 왜 환경변수인가 — 0.2.1 의 프로세스 계보(bind.ts)는 훅이 매 세션 `Win32_Process` 전체를
|
|
6
|
+
* 훑어(~450ms) 조상 PID 를 보내고 플러그인이 탭별 셸 PID 와 맞추는 방식이다. 맞기는 하지만
|
|
7
|
+
* 훅이 느리고, 셸 PID 를 pty 에 비동기로 물어야 해서 표가 늦게 채워지는 창이 있다.
|
|
8
|
+
* 플러그인이 탭을 열 때 셸 환경에 `AGENTDECK_TAB=<id>` 를 심어 두면 그 셸에서 뜬 claude 와
|
|
9
|
+
* 그 훅은 환경을 그대로 물려받으므로, 훅은 `$env:AGENTDECK_TAB` 을 읽어 보내기만 하면 되고
|
|
10
|
+
* 플러그인은 표에서 바로 찾는다. 계보 조회가 통째로 빠진다.
|
|
11
|
+
*
|
|
12
|
+
* 계보는 폴백으로 남는다 — 이 버전 이전에 열린 탭, 재시작으로 복원된 탭(셸이 이미 떠 있어
|
|
13
|
+
* 새 값을 심을 수 없다), 사람이 셸 밖에서 부른 훅은 환경변수가 없다.
|
|
14
|
+
*/
|
|
15
|
+
export declare const TAB_ENV = "AGENTDECK_TAB";
|
|
16
|
+
/**
|
|
17
|
+
* 탭 하나를 가리키는 짧은 무작위 id — 12 hex 자리(48비트). 같은 Tabby 안에서만 유일하면 되므로
|
|
18
|
+
* 이 길이로 충분하고, 셸 프롬프트나 진단 로그에 찍혀도 거슬리지 않는다.
|
|
19
|
+
* Electron 렌더러와 Node 20 은 `globalThis.crypto.getRandomValues` 를 둘 다 갖고 있다.
|
|
20
|
+
*/
|
|
21
|
+
export declare function newTabId(): string;
|
|
22
|
+
/**
|
|
23
|
+
* 훅이 보낸 tabId 를 가진 탭(루트)을 찾는다. 분할 탭은 패널마다 id 가 다르므로 루트 하나에
|
|
24
|
+
* 여러 id 가 달린다. 없으면 null — 추측하지 않는다 (호출자가 계보/폴백으로 넘어간다).
|
|
25
|
+
*/
|
|
26
|
+
export declare function pickTabByTabId<T>(tabId: string | undefined, tabIds: Map<T, string[]>): T | null;
|
|
@@ -18,15 +18,23 @@
|
|
|
18
18
|
프롬프트 컨텍스트로 주입되기 때문이다.
|
|
19
19
|
|
|
20
20
|
.PARAMETER Status
|
|
21
|
-
running(진행중) / waiting(승인대기) / done(완료) / error(오류) / idle(대기)
|
|
21
|
+
running(진행중) / waiting(승인대기) / limited(한도 도달) / done(완료) / error(오류) / idle(대기)
|
|
22
|
+
|
|
23
|
+
StopFailure 훅에서는 -Status 를 그대로 쓰지 않는다 — 훅 JSON 의 `error` 가 rate_limit(사용량 한도)이면
|
|
24
|
+
limited 로, 그 밖이면 error 로 보낸다. 이벤트 하나에 matcher 두 개를 거는 대신 여기서 가른다.
|
|
22
25
|
|
|
23
26
|
.EXAMPLE
|
|
24
|
-
# settings.json
|
|
25
|
-
# "UserPromptSubmit":
|
|
26
|
-
# "
|
|
27
|
+
# settings.json (src/claudeHooks.ts PLAN 이 원본)
|
|
28
|
+
# "UserPromptSubmit": -Status running 지시를 받았다
|
|
29
|
+
# "Notification": -Status waiting 승인을 기다린다 (유휴 알림은 걸러낸다)
|
|
30
|
+
# "PostToolUse": -Status running 승인 뒤 도구가 돌았다
|
|
31
|
+
# "PostToolUseFailure": -Status running 도구가 실패했다 — 에이전트는 계속 일한다
|
|
32
|
+
# "PermissionDenied": -Status running 사람이 거부했다 — 에이전트는 계속 일한다
|
|
33
|
+
# "Stop": -Status done 답이 끝났다
|
|
34
|
+
# "StopFailure": -Status error 턴이 실패로 끝났다 (rate_limit 이면 limited)
|
|
27
35
|
#>
|
|
28
36
|
param(
|
|
29
|
-
[ValidateSet('running', 'waiting', 'done', 'error', 'idle')]
|
|
37
|
+
[ValidateSet('running', 'waiting', 'limited', 'done', 'error', 'idle')]
|
|
30
38
|
[string]$Status,
|
|
31
39
|
|
|
32
40
|
# 사람이 셸에서 직접 붙이는 작업 이름. 이때만 라벨을 보낸다
|
|
@@ -76,6 +84,7 @@ $safeId = ($targetId -replace '[^A-Za-z0-9._-]', '_')
|
|
|
76
84
|
$file = Join-Path $dir "$safeId.json"
|
|
77
85
|
|
|
78
86
|
# 직전 상태를 읽어 둔다 — 승인대기 오탐 방지에 쓴다
|
|
87
|
+
$prev = $null
|
|
79
88
|
$prevStatus = ''
|
|
80
89
|
if (Test-Path $file) {
|
|
81
90
|
try {
|
|
@@ -84,6 +93,94 @@ if (Test-Path $file) {
|
|
|
84
93
|
} catch { }
|
|
85
94
|
}
|
|
86
95
|
|
|
96
|
+
# --- StopFailure: 한도 도달인가 그냥 오류인가 ---
|
|
97
|
+
# 턴이 실패로 끝나면 Claude Code 는 Stop 대신 StopFailure 를 쏘고 `error` 에 원인 코드를 싣는다
|
|
98
|
+
# (rate_limit / overloaded / server_error / authentication_failed / max_output_tokens …, 2.1.263 실측).
|
|
99
|
+
# 사용량 한도(429 "You've hit your session limit · resets 12pm")는 error=rate_limit 이다 — 사람이 승인할 것이
|
|
100
|
+
# 없고 리셋까지 기다려야 하는 상태라 승인대기·오류와 구분해 limited 로 보낸다. 나머지는 error.
|
|
101
|
+
# 부연(reason)에는 limited 면 화면에 찍힌 마지막 문장(last_assistant_message: 리셋 시각이 들어 있다)을,
|
|
102
|
+
# 그 밖의 오류면 error 코드를 싣는다. 줄이는 일은 플러그인(src/reason.ts)이 한다.
|
|
103
|
+
$failReason = ''
|
|
104
|
+
if ($hook -and [string]$hook.hook_event_name -eq 'StopFailure') {
|
|
105
|
+
$code = [string]$hook.error
|
|
106
|
+
$Status = if ($code -eq 'rate_limit') { 'limited' } else { 'error' }
|
|
107
|
+
# limited 는 화면 문구(리셋 시각이 들어 있다), 그 밖은 error 코드만 — 상세(error_details)는 배지 한 줄에 안 들어간다
|
|
108
|
+
$failReason = if ($Status -eq 'limited' -and $hook.last_assistant_message) { [string]$hook.last_assistant_message } else { $code }
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
# --- 진행중 중복 보고 생략 ---
|
|
112
|
+
# PostToolUse 는 도구가 돌 때마다 발화한다. 이미 running 인데 running 을 또 쓰면 파일·TCP 만 흔들고
|
|
113
|
+
# 사이드바는 변하지 않는다(경과시간이 매 도구마다 0 으로 되돌아가기까지 한다). 라벨 지정도 없으면 여기서 끝낸다 —
|
|
114
|
+
# 아래 계보 조회(~450ms)까지 건너뛰므로 도구마다 붙는 비용이 사실상 없다
|
|
115
|
+
if ($Status -eq 'running' -and $prevStatus -eq 'running' -and -not $Label) { exit 0 }
|
|
116
|
+
|
|
117
|
+
# --- 어느 탭인가: tabId (1순위) / 프로세스 계보 pids (폴백) ---
|
|
118
|
+
# 사이드바가 이 보고를 어느 탭에 붙일지 정하는 근거. 예전 규칙 "처음 보고할 때의 활성 탭" 은
|
|
119
|
+
# 끝난 세션의 done 이 새 탭에 박히는 사고를 냈다 (2026-09-02 실측). 지금은 두 단계다.
|
|
120
|
+
#
|
|
121
|
+
# 1) tabId — 플러그인이 탭을 열 때 그 셸의 환경에 AGENTDECK_TAB=<id> 를 심는다 (src/tabenv.ts,
|
|
122
|
+
# notify.service.ts stampRoot). 그 셸에서 뜬 claude 와 이 훅은 환경을 그대로 물려받으므로
|
|
123
|
+
# $env:AGENTDECK_TAB 을 읽어 보내면 플러그인이 표에서 바로 찾는다. 이게 있으면 아래 계보 조회를
|
|
124
|
+
# 통째로 건너뛴다 — Win32_Process 전체 조회(~450ms)가 빠져 훅이 수십 ms 안에 끝난다.
|
|
125
|
+
# $prev 에 남은 pids 캐시는 읽지도 않는다 — payload 에 pids 를 넣지 않으니 무해하다.
|
|
126
|
+
#
|
|
127
|
+
# 2) pids — 환경변수가 없는 셸: 이 버전 이전에 열린 탭, 재시작으로 복원된 탭(셸이 이미 떠 있어 새 값을
|
|
128
|
+
# 못 심는다), Tabby 밖에서 뜬 claude. 훅 프로세스의 조상을 위로 따라가면
|
|
129
|
+
# (훅 pwsh) <- bash <- claude.exe <- 탭 셸(powershell/pwsh) <- Tabby.exe 가 나오고,
|
|
130
|
+
# 플러그인은 탭별 셸 PID 와 맞춰 어느 탭인지 확정한다 (src/bind.ts).
|
|
131
|
+
#
|
|
132
|
+
# 훅 JSON 이 있을 때만 계산한다 — 셸에서 사람이 -SessionId 로 직접 부르면 조상이 탭 셸 자체라
|
|
133
|
+
# 걸러지고 Tabby.exe 만 남아 어느 탭에도 안 맞는다. 그 경로는 예전 활성 탭 규칙에 맡긴다.
|
|
134
|
+
#
|
|
135
|
+
# 비용 — Win32_Process 전체 조회가 ~450ms 라 세션당 한 번만 하고 status 파일에 캐시한다
|
|
136
|
+
# (홉마다 필터 조회하면 ~2초로 더 느리다). 캐시는 claude 프로세스(claudePid)가 살아 있고
|
|
137
|
+
# 이름이 같을 때만 재사용한다 — `claude --resume` 을 다른 탭에서 띄우면 PID 가 바뀌어 다시 계산된다.
|
|
138
|
+
$tabId = ([string]$env:AGENTDECK_TAB).Trim()
|
|
139
|
+
$pids = @()
|
|
140
|
+
$claudePid = 0
|
|
141
|
+
$claudeName = ''
|
|
142
|
+
if ($tabId) {
|
|
143
|
+
# 탭이 확정됐다 — 계보는 계산하지 않는다
|
|
144
|
+
} elseif ($hook) {
|
|
145
|
+
$reuse = $false
|
|
146
|
+
if ($prev -and $prev.pids -and $prev.claudePid) {
|
|
147
|
+
$aliveProc = Get-Process -Id ([int]$prev.claudePid) -ErrorAction SilentlyContinue
|
|
148
|
+
if ($aliveProc -and (("$($aliveProc.ProcessName).exe") -ieq [string]$prev.claudeName)) {
|
|
149
|
+
$pids = @($prev.pids | ForEach-Object { [int]$_ })
|
|
150
|
+
$claudePid = [int]$prev.claudePid
|
|
151
|
+
$claudeName = [string]$prev.claudeName
|
|
152
|
+
$reuse = $true
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
if (-not $reuse) {
|
|
156
|
+
# 훅 자신과 그것을 띄운 셸 껍데기 — 매번 바뀌는 일회성 프로세스라 맞춰 볼 대상이 아니다.
|
|
157
|
+
# 앞에서부터 이 이름들을 건너뛰고 처음 만나는 것이 에이전트 프로세스(claude.exe / node.exe)다.
|
|
158
|
+
# 그 뒤의 탭 셸(powershell.exe)은 접두만 건너뛰므로 남는다
|
|
159
|
+
$transient = @('powershell.exe', 'pwsh.exe', 'bash.exe', 'sh.exe', 'cmd.exe', 'conhost.exe')
|
|
160
|
+
try {
|
|
161
|
+
$all = @{}
|
|
162
|
+
Get-CimInstance Win32_Process -Property ProcessId, ParentProcessId, Name -ErrorAction Stop |
|
|
163
|
+
ForEach-Object { $all[[int]$_.ProcessId] = $_ }
|
|
164
|
+
$chain = @()
|
|
165
|
+
$cur = [int]$PID
|
|
166
|
+
for ($i = 0; $i -lt 16 -and $cur -gt 0 -and $all.ContainsKey($cur); $i++) {
|
|
167
|
+
$row = $all[$cur]
|
|
168
|
+
$chain += $row
|
|
169
|
+
$next = [int]$row.ParentProcessId
|
|
170
|
+
if ($next -eq $cur) { break }
|
|
171
|
+
$cur = $next
|
|
172
|
+
}
|
|
173
|
+
$start = 0
|
|
174
|
+
while ($start -lt $chain.Count -and ($transient -contains ([string]$chain[$start].Name).ToLower())) { $start++ }
|
|
175
|
+
if ($start -lt $chain.Count) {
|
|
176
|
+
$claudePid = [int]$chain[$start].ProcessId
|
|
177
|
+
$claudeName = [string]$chain[$start].Name
|
|
178
|
+
$pids = @($chain[$start..($chain.Count - 1)] | ForEach-Object { [int]$_.ProcessId })
|
|
179
|
+
}
|
|
180
|
+
} catch { }
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
87
184
|
# --- 승인대기 오탐 차단 ---
|
|
88
185
|
# Notification 훅은 "도구 승인 요청" 말고 "입력이 없다"는 유휴 알림에도 발화한다.
|
|
89
186
|
# 그래서 Stop(done) 뒤에 유휴 알림이 오면 완료된 탭이 '승인대기' 로 뒤집힌다 (2026-08-28 실측:
|
|
@@ -92,10 +189,26 @@ if (Test-Path $file) {
|
|
|
92
189
|
if ($Status -eq 'waiting') {
|
|
93
190
|
$msg = if ($hook -and $hook.message) { [string]$hook.message } else { '' }
|
|
94
191
|
$idleNotice = $msg -match '(?i)waiting for your input|idle'
|
|
95
|
-
$alreadyFinished = $prevStatus -eq 'done' -or $prevStatus -eq 'idle'
|
|
192
|
+
$alreadyFinished = $prevStatus -eq 'done' -or $prevStatus -eq 'idle' -or $prevStatus -eq 'limited'
|
|
96
193
|
if ($idleNotice -or $alreadyFinished) { exit 0 }
|
|
97
194
|
}
|
|
98
195
|
|
|
196
|
+
# --- 부연(reason) ---
|
|
197
|
+
# waiting: Notification 의 message 원문("Claude needs your permission to use Bash" 등).
|
|
198
|
+
# limited/error: 위 StopFailure 블록이 고른 문장.
|
|
199
|
+
# 짧게 줄이는 일(-> "Bash 권한", "12pm 리셋")은 플러그인(src/reason.ts)이 한다 — 문구 규칙이 바뀌면 한 곳만 고치면 된다.
|
|
200
|
+
# 해당 상태일 때만 넣고 다른 상태에서는 키 자체를 빼서, 플러그인이 상태가 바뀌면 이유를 비우게 한다.
|
|
201
|
+
$reason = ''
|
|
202
|
+
if ($Status -eq 'waiting' -and $hook -and $hook.message) {
|
|
203
|
+
$reason = [string]$hook.message
|
|
204
|
+
} elseif ($failReason) {
|
|
205
|
+
$reason = $failReason
|
|
206
|
+
}
|
|
207
|
+
if ($reason) {
|
|
208
|
+
$reason = ($reason -replace '\s+', ' ').Trim()
|
|
209
|
+
if ($reason.Length -gt 120) { $reason = $reason.Substring(0, 120).TrimEnd() }
|
|
210
|
+
}
|
|
211
|
+
|
|
99
212
|
# --- 라벨 결정 ---
|
|
100
213
|
# **이 훅은 라벨을 만들지 않는다.** 작업 이름은 플러그인이 Enter 시점에 입력창을 읽어
|
|
101
214
|
# 무조건 갱신하는 경로(claimEnterLabel / enterAsLabel)가 전담한다. 훅이 낼 수 있는 건
|
|
@@ -121,6 +234,13 @@ $payload = [ordered]@{
|
|
|
121
234
|
ts = [int64](([datetime]::UtcNow - [datetime]'1970-01-01').TotalMilliseconds)
|
|
122
235
|
}
|
|
123
236
|
if ($newLabel) { $payload['label'] = $newLabel }
|
|
237
|
+
if ($reason) { $payload['reason'] = $reason }
|
|
238
|
+
if ($tabId) { $payload['tabId'] = $tabId }
|
|
239
|
+
if ($pids.Count -gt 0) {
|
|
240
|
+
$payload['pids'] = [int[]]$pids
|
|
241
|
+
$payload['claudePid'] = $claudePid
|
|
242
|
+
$payload['claudeName'] = $claudeName
|
|
243
|
+
}
|
|
124
244
|
|
|
125
245
|
# BOM 없는 UTF-8 로 원자적 쓰기 — Node 의 JSON.parse 는 BOM 을 못 먹는다
|
|
126
246
|
#
|
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
ConPTY 가 API 로 변환해 전달하므로 확실히 통과한다.
|
|
12
12
|
|
|
13
13
|
.PARAMETER Status
|
|
14
|
-
running(진행중) / waiting(승인대기) / done(완료) / error(오류) / idle(대기)
|
|
14
|
+
running(진행중) / waiting(승인대기) / limited(한도 도달) / done(완료) / error(오류) / idle(대기)
|
|
15
15
|
|
|
16
16
|
.PARAMETER Label
|
|
17
17
|
사이드바에 함께 보일 작업 이름.
|
|
@@ -22,7 +22,7 @@
|
|
|
22
22
|
#>
|
|
23
23
|
param(
|
|
24
24
|
[Parameter(Mandatory = $true)]
|
|
25
|
-
[ValidateSet('running', 'waiting', 'done', 'error', 'idle')]
|
|
25
|
+
[ValidateSet('running', 'waiting', 'limited', 'done', 'error', 'idle')]
|
|
26
26
|
[string]$Status,
|
|
27
27
|
|
|
28
28
|
[string]$Label = ''
|
package/install.ps1
CHANGED
|
@@ -1,17 +1,12 @@
|
|
|
1
1
|
<#
|
|
2
2
|
.SYNOPSIS
|
|
3
|
-
AgentDeck 플러그인을 Tabby 에
|
|
3
|
+
AgentDeck 플러그인을 Tabby 에 연결한다.
|
|
4
4
|
|
|
5
5
|
.DESCRIPTION
|
|
6
6
|
Tabby 는 사용자 플러그인 디렉토리(userData/plugins/node_modules)를 스캔하므로
|
|
7
7
|
이 스크립트는 그곳에 프로젝트 폴더를 가리키는 junction 을 만든다.
|
|
8
8
|
소스를 고치고 `npm run build` 만 하면 Tabby 재시작으로 바로 반영된다.
|
|
9
9
|
|
|
10
|
-
.PARAMETER Shortcut
|
|
11
|
-
바탕화면에 "Tabby (AgentDeck)" 관리자 권한 바로가기를 만든다.
|
|
12
|
-
Tabby 를 관리자로 띄우면 그 안에서 열리는 모든 탭이 권한을 물려받으므로
|
|
13
|
-
탭마다 UAC 창이 뜨는 것을 피할 수 있다.
|
|
14
|
-
|
|
15
10
|
.PARAMETER Hooks
|
|
16
11
|
Claude Code 설정(~/.claude/settings.json)에 상태 통보 훅을 등록한다.
|
|
17
12
|
|
|
@@ -20,10 +15,9 @@
|
|
|
20
15
|
등록하지 않아도 동작은 하지만(출력 패턴 폴백) 판정이 그만큼 흔들린다.
|
|
21
16
|
|
|
22
17
|
.EXAMPLE
|
|
23
|
-
powershell -NoProfile -ExecutionPolicy Bypass -File install.ps1 -
|
|
18
|
+
powershell -NoProfile -ExecutionPolicy Bypass -File install.ps1 -Hooks
|
|
24
19
|
#>
|
|
25
20
|
param(
|
|
26
|
-
[switch]$Shortcut,
|
|
27
21
|
[switch]$Hooks
|
|
28
22
|
)
|
|
29
23
|
|
|
@@ -113,28 +107,5 @@ if ($Hooks) {
|
|
|
113
107
|
Write-Host "저장: $settingsPath (백업: $settingsPath.agentdeck-backup)"
|
|
114
108
|
}
|
|
115
109
|
|
|
116
|
-
# 3) 관리자 권한 바로가기
|
|
117
|
-
if ($Shortcut) {
|
|
118
|
-
$exe = Join-Path $env:LOCALAPPDATA 'Programs\Tabby\Tabby.exe'
|
|
119
|
-
if (-not (Test-Path $exe)) {
|
|
120
|
-
throw "Tabby.exe 를 찾지 못했다: $exe"
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
$lnkPath = Join-Path ([Environment]::GetFolderPath('Desktop')) 'Tabby (AgentDeck).lnk'
|
|
124
|
-
$shell = New-Object -ComObject WScript.Shell
|
|
125
|
-
$lnk = $shell.CreateShortcut($lnkPath)
|
|
126
|
-
$lnk.TargetPath = $exe
|
|
127
|
-
$lnk.WorkingDirectory = Split-Path $exe
|
|
128
|
-
$lnk.Description = 'Tabby + AgentDeck (관리자 권한)'
|
|
129
|
-
$lnk.Save()
|
|
130
|
-
|
|
131
|
-
# 바로가기에 "관리자 권한으로 실행" 플래그를 세운다 (.lnk 바이너리의 21번째 바이트 0x20 비트)
|
|
132
|
-
$bytes = [System.IO.File]::ReadAllBytes($lnkPath)
|
|
133
|
-
$bytes[0x15] = $bytes[0x15] -bor 0x20
|
|
134
|
-
[System.IO.File]::WriteAllBytes($lnkPath, $bytes)
|
|
135
|
-
|
|
136
|
-
Write-Host "바로가기 생성: $lnkPath (관리자 권한)"
|
|
137
|
-
}
|
|
138
|
-
|
|
139
110
|
Write-Host ""
|
|
140
111
|
Write-Host "Tabby 를 재시작하면 적용된다."
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "tabby-agentdeck",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
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 src/order.ts src/reason.ts src/tabenv.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 && node test/order.test.js && node test/reason.test.js && node test/tabenv.test.js"
|
|
20
20
|
},
|
|
21
21
|
"files": [
|
|
22
22
|
"dist",
|