tabby-agentdeck 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +240 -0
- package/dist/api.d.ts +21 -0
- package/dist/config.d.ts +179 -0
- package/dist/deck.service.d.ts +331 -0
- package/dist/decorator.d.ts +28 -0
- package/dist/detect.d.ts +15 -0
- package/dist/hotkeys.d.ts +12 -0
- package/dist/index.d.ts +8 -0
- package/dist/index.js +3535 -0
- package/dist/index.js.map +1 -0
- package/dist/notify.service.d.ts +89 -0
- package/dist/profile.service.d.ts +16 -0
- package/dist/prompt.d.ts +26 -0
- package/dist/status.service.d.ts +32 -0
- package/hooks/agentdeck-label.js +53 -0
- package/hooks/agentdeck-notify.ps1 +159 -0
- package/hooks/agentdeck-status.ps1 +33 -0
- package/install.ps1 +82 -0
- package/package.json +50 -0
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import { AppService, ConfigService } from 'tabby-core';
|
|
2
|
+
import { WorkStatusService } from './status.service';
|
|
3
|
+
/**
|
|
4
|
+
* 외부 도구 -> 사이드바 상태 채널. **기본은 꺼져 있다** (`notifyChannel`).
|
|
5
|
+
*
|
|
6
|
+
* 라벨은 Enter 시점 입력창에서, 상태는 출력 패턴에서 플러그인이 스스로 얻는 것이 기본 경로다
|
|
7
|
+
* (claimEnterLabel / detect.ts). 설정 파일을 손봐야 도는 훅은 배포판에서 기대할 수 없어서
|
|
8
|
+
* 라벨 원천에서 내렸다. 이 채널은 외부 도구가 상태를 직접 보고하고 싶을 때만 켠다.
|
|
9
|
+
*
|
|
10
|
+
* 왜 파일인가 — 콘솔 제목 방식(`hooks/agentdeck-status.ps1`)은 **훅에서는 통하지 않는다**.
|
|
11
|
+
* Claude Code 는 훅을 파이프 stdio 로 띄우기 때문에 그 자식 프로세스는 터미널의 콘솔에
|
|
12
|
+
* 붙어 있지 않고, 제목을 바꿔도 pty 까지 오지 않는다 (2026-08-28 실측: 훅으로
|
|
13
|
+
* `[AD:waiting] ...` 을 걸어도 사이드바가 그대로였다). 셸 안에서 직접 실행할 때는
|
|
14
|
+
* 제목 방식이 여전히 유효하므로 그 경로는 남겨 둔다.
|
|
15
|
+
*
|
|
16
|
+
* 전달 경로는 두 가닥이다.
|
|
17
|
+
*
|
|
18
|
+
* 1. **TCP (기본)** — 이쪽이 `127.0.0.1` 임의 포트로 듣고, 실제 포트를
|
|
19
|
+
* `%LOCALAPPDATA%\tabby-agentdeck\port` 에 적어 둔다. 훅은 그 포트로 JSON 한 줄을
|
|
20
|
+
* 보내고 끊는다. 폴링 지연이 없고 상태 파일도 남지 않는다.
|
|
21
|
+
* 2. **파일 (폴백)** — TCP 연결이 안 되면(Tabby 미기동, 포트 파일 없음, 방화벽)
|
|
22
|
+
* 훅이 `...\status\<sessionId>.json` 에 쓰고 이쪽이 400ms 마다 읽는다.
|
|
23
|
+
*
|
|
24
|
+
* 두 경로 모두 `apply()` 로 모이고, `applied` 의 ts 대조가 중복 적용을 막으므로
|
|
25
|
+
* 같은 보고가 양쪽으로 들어와도 결과는 같다.
|
|
26
|
+
*
|
|
27
|
+
* 세션 -> 탭 매칭: 어떤 Claude 세션이 **처음** 보고할 때의 활성 탭에 묶는다.
|
|
28
|
+
* 사용자가 프롬프트를 넣은 탭이 곧 활성 탭이므로 이 규칙이면 탭이 여러 개여도 맞다.
|
|
29
|
+
* 한 번 묶으면 그 뒤로는 활성 탭이 바뀌어도 같은 탭을 따라간다.
|
|
30
|
+
*/
|
|
31
|
+
export declare class WorkNotifyService {
|
|
32
|
+
private app;
|
|
33
|
+
private config;
|
|
34
|
+
private status;
|
|
35
|
+
private root;
|
|
36
|
+
private dir;
|
|
37
|
+
/** 훅이 접속할 포트를 알려주는 파일 */
|
|
38
|
+
private portFile;
|
|
39
|
+
private server;
|
|
40
|
+
private sessionTab;
|
|
41
|
+
/** 역방향 — 한 탭을 두 세션이 나눠 쓰지 못하게 막는다 */
|
|
42
|
+
private tabSession;
|
|
43
|
+
/** 이미 적용한 (sessionId -> ts) — 같은 파일을 반복 적용하지 않는다 */
|
|
44
|
+
private applied;
|
|
45
|
+
private timer;
|
|
46
|
+
/** 폴링을 시작한 시각 — 이보다 오래된 파일은 지난 실행의 잔재다 */
|
|
47
|
+
private startedAt;
|
|
48
|
+
constructor(app: AppService, config: ConfigService, status: WorkStatusService);
|
|
49
|
+
init(): void;
|
|
50
|
+
/** 진단용 */
|
|
51
|
+
get debugDir(): string;
|
|
52
|
+
/** 진단용 — 듣고 있는 포트 (0 = TCP 채널이 안 떴다) */
|
|
53
|
+
get debugPort(): number;
|
|
54
|
+
/**
|
|
55
|
+
* 훅이 상태를 바로 밀어 넣을 TCP 채널을 연다.
|
|
56
|
+
*
|
|
57
|
+
* 기본은 `agentDeck.notifyPort` 고정 포트다 — 번호가 늘 같아야 훅이나 진단 도구가
|
|
58
|
+
* 바로 붙을 수 있다. Windows 임시 포트 범위(49152~65535) 아래로 잡아 두어야
|
|
59
|
+
* OS 가 그 번호를 다른 프로세스에 먼저 내주는 일이 없다.
|
|
60
|
+
*
|
|
61
|
+
* 그 번호가 이미 물려 있으면(Tabby 두 개, 다른 프로그램) 0 으로 다시 시도한다 —
|
|
62
|
+
* OS 가 빈 포트를 골라 준다. 어느 쪽이든 실제 번호는 port 파일에 적어 두므로
|
|
63
|
+
* 훅은 고정/임의를 구분할 필요가 없다. 둘 다 실패해도 파일 폴백이 살아 있다.
|
|
64
|
+
*/
|
|
65
|
+
private listen;
|
|
66
|
+
/** TCP 로 들어온 JSON 한 줄 */
|
|
67
|
+
private accept;
|
|
68
|
+
private poll;
|
|
69
|
+
/**
|
|
70
|
+
* 보고 한 건을 반영한다. `status` / `label` 은 **각각 선택**이고, 빠진 쪽은 지금 값을 유지한다.
|
|
71
|
+
*
|
|
72
|
+
* 병합 규칙을 여기(플러그인)에 둔 이유 — 예전에는 훅 스크립트가 status 파일을 읽어
|
|
73
|
+
* 직전 값과 합친 뒤 다시 쓰는 read-modify-write 를 했다. 그러면 진짜 상태가 파일과
|
|
74
|
+
* 플러그인 메모리 두 곳에 살고 병합 규칙이 PowerShell 안에 숨는다. 라벨만 바꾸려 해도
|
|
75
|
+
* 직전 status 를 채워 넣어야 했고(안 채우면 여기서 통째로 버려졌다), 파일이 밀리면
|
|
76
|
+
* 표시가 어긋났다. 보내는 쪽은 "자기가 아는 것만" 보내고 합치는 건 상태를 실제로
|
|
77
|
+
* 들고 있는 이쪽이 한다 — 그래야 어느 경로로 들어오든 결과가 같다.
|
|
78
|
+
*/
|
|
79
|
+
private apply;
|
|
80
|
+
/**
|
|
81
|
+
* 세션에 묶인 탭을 찾는다. 처음 보는 세션이면 지금 활성 탭에 묶는다.
|
|
82
|
+
*
|
|
83
|
+
* 단 그 탭을 이미 다른 세션이 점유했으면 **묶지 않는다**. 예전엔 이 검사가 없어서
|
|
84
|
+
* 여러 세션이 한 탭에 겹쳐 붙었고, 서로의 상태를 덮어써 "작업 중인 탭이 완료로,
|
|
85
|
+
* 끝난 탭이 진행 중으로" 뒤바뀌어 보였다. 못 묶은 세션은 사용자가 그 탭을 활성화하는
|
|
86
|
+
* 순간(= 다음 보고 때) 제 짝을 찾는다.
|
|
87
|
+
*/
|
|
88
|
+
private resolveTab;
|
|
89
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { AppService, ConfigService } from 'tabby-core';
|
|
2
|
+
export declare const ROOT_PROFILE_ID = "agentdeck:root";
|
|
3
|
+
/**
|
|
4
|
+
* 새 탭이 항상 작업 루트에서 열리게 한다.
|
|
5
|
+
*
|
|
6
|
+
* Tabby 프로필을 하나 만들어 config 에 심고, 사용자가 아직 기본 프로필을
|
|
7
|
+
* 직접 고른 적이 없으면 이것을 기본으로 지정한다.
|
|
8
|
+
* 한 번 심은 뒤에는 사용자가 설정에서 바꾼 값을 존중한다 (매 기동 덮어쓰지 않음).
|
|
9
|
+
*/
|
|
10
|
+
export declare class AgentDeckProfileService {
|
|
11
|
+
private app;
|
|
12
|
+
private config;
|
|
13
|
+
constructor(app: AppService, config: ConfigService);
|
|
14
|
+
init(): void;
|
|
15
|
+
private ensureProfile;
|
|
16
|
+
}
|
package/dist/prompt.d.ts
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 터미널 화면에 그려진 입력창에서 지금 쓰여 있는 프롬프트를 뽑아낸다.
|
|
3
|
+
*
|
|
4
|
+
* 타이핑을 따로 세지 않고 화면을 읽는 이유 — 붙여넣기·↑ 히스토리·자동완성·IME 로 들어온 글자는
|
|
5
|
+
* 키 이벤트로 안 오거나 원문과 다르다. 화면에 실제로 그려진 것이 곧 보낼 내용이다.
|
|
6
|
+
*
|
|
7
|
+
* 입력창 모양은 앱·버전마다 다르다. 옛 Claude Code / Codex 는 테두리 상자였고
|
|
8
|
+
* ╭────────────────────╮
|
|
9
|
+
* │ > 프롬프트 내용 │
|
|
10
|
+
* ╰────────────────────╯
|
|
11
|
+
* Claude Code 2.1.x 는 상자를 버리고 위아래 가로선 사이에 한 줄만 그린다.
|
|
12
|
+
* ────────────────────────
|
|
13
|
+
* ❯ 프롬프트 내용
|
|
14
|
+
* ────────────────────────
|
|
15
|
+
* ? for shortcuts
|
|
16
|
+
* 머리글자도 ASCII `>` 가 아니라 `❯`(U+276F) 다 — 2026-09-01 화면 캡처로 확인.
|
|
17
|
+
* `>` 만 찾던 정규식이 이걸 못 잡아 라벨이 한 번도 안 바뀌었다.
|
|
18
|
+
*
|
|
19
|
+
* 머리를 못 찾으면 빈 문자열을 준다 — 일반 셸이나 y/n 확인 같은 데서 엉뚱한 라벨이 박히느니
|
|
20
|
+
* 라벨을 그대로 두는 편이 낫다.
|
|
21
|
+
*
|
|
22
|
+
* 화면 버퍼를 직접 받지 않고 줄 배열만 받는 순수 함수로 둔다 (xterm 없이 검증할 수 있게).
|
|
23
|
+
*/
|
|
24
|
+
/** 사이드바에 들어가는 라벨 길이 */
|
|
25
|
+
export declare const MAX_LABEL_LEN = 40;
|
|
26
|
+
export declare function extractPrompt(lines: string[]): string;
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { Observable } from 'rxjs';
|
|
2
|
+
import { BaseTabComponent } from 'tabby-core';
|
|
3
|
+
import { TabState, WorkStatus } from './api';
|
|
4
|
+
/**
|
|
5
|
+
* 탭별 작업 상태 보관소.
|
|
6
|
+
* 상태의 출처는 3가지 — 자동 감지(decorator), OSC 시퀀스(Claude Code 훅), 사이드바에서의 수동 지정.
|
|
7
|
+
* pinned 가 true 면 자동 감지가 값을 덮어쓰지 않는다.
|
|
8
|
+
*/
|
|
9
|
+
export declare class WorkStatusService {
|
|
10
|
+
private states;
|
|
11
|
+
private changed;
|
|
12
|
+
/** 진단용 카운터 — decorator 가 실제로 붙었는지, 출력이 흘러오는지 확인한다 */
|
|
13
|
+
debug: {
|
|
14
|
+
attached: number;
|
|
15
|
+
outputs: number;
|
|
16
|
+
oscHits: number;
|
|
17
|
+
titleHits: number;
|
|
18
|
+
};
|
|
19
|
+
get changed$(): Observable<void>;
|
|
20
|
+
get(tab: BaseTabComponent): TabState;
|
|
21
|
+
/** 자동 감지용 — pinned 상태는 건드리지 않는다 */
|
|
22
|
+
setAuto(tab: BaseTabComponent, status: WorkStatus): void;
|
|
23
|
+
/** 수동/훅 지정 — 이후 자동 감지보다 우선한다 */
|
|
24
|
+
setManual(tab: BaseTabComponent, status: WorkStatus, label?: string): void;
|
|
25
|
+
setLabel(tab: BaseTabComponent, label: string): void;
|
|
26
|
+
/** 수동 고정을 풀고 자동 감지로 되돌린다 */
|
|
27
|
+
unpin(tab: BaseTabComponent): void;
|
|
28
|
+
markOutput(tab: BaseTabComponent): void;
|
|
29
|
+
forget(tab: BaseTabComponent): void;
|
|
30
|
+
/** 사이드바가 매 틱 호출 — idle 전환만 담당하고 화면 갱신 필요 여부를 돌려준다 */
|
|
31
|
+
tickIdle(tab: BaseTabComponent, idleAfterMs: number): boolean;
|
|
32
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* 작업 라벨만 바꾼다 (상태는 건드리지 않는다).
|
|
4
|
+
*
|
|
5
|
+
* node agentdeck-label.js <sessionId> "회랑 정산 버그 수정"
|
|
6
|
+
* AGENTDECK_SESSION=<id> node agentdeck-label.js "빌드 중"
|
|
7
|
+
*
|
|
8
|
+
* 에이전트가 단계(분석→구현→빌드→검증)를 넘어갈 때마다 부르는 용도다.
|
|
9
|
+
* PowerShell 훅(agentdeck-notify.ps1)은 프로세스 기동만 100ms 대라 자주 부르기엔 무겁고,
|
|
10
|
+
* 그쪽은 status 파일을 읽어 직전 값과 합치는 read-modify-write 를 한다. 여기서는
|
|
11
|
+
* 아는 것(라벨)만 보내고 병합은 플러그인이 한다 (notify.service.ts apply 주석 참고).
|
|
12
|
+
*
|
|
13
|
+
* TCP(127.0.0.1:47500)로 한 줄 밀어넣고, Tabby 가 안 떠 있으면 status 파일로 폴백한다.
|
|
14
|
+
* 파일도 부분 갱신을 이해하므로 status 를 지어내지 않는다.
|
|
15
|
+
*/
|
|
16
|
+
const net = require('net')
|
|
17
|
+
const fs = require('fs')
|
|
18
|
+
const path = require('path')
|
|
19
|
+
|
|
20
|
+
const args = process.argv.slice(2)
|
|
21
|
+
const sessionId = args.length > 1 ? args[0] : process.env.AGENTDECK_SESSION
|
|
22
|
+
const label = (args.length > 1 ? args[1] : args[0] || '').trim()
|
|
23
|
+
const port = Number(process.env.AGENTDECK_PORT || 47500)
|
|
24
|
+
|
|
25
|
+
if (!sessionId || !label) {
|
|
26
|
+
console.error('usage: agentdeck-label.js <sessionId> "<label>" (or AGENTDECK_SESSION env)')
|
|
27
|
+
process.exit(2)
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const payload = JSON.stringify({ sessionId, label, ts: Date.now() }) + '\n'
|
|
31
|
+
|
|
32
|
+
const fallback = () => {
|
|
33
|
+
const dir = path.join(process.env.LOCALAPPDATA || process.env.HOME, 'tabby-agentdeck', 'status')
|
|
34
|
+
try {
|
|
35
|
+
fs.mkdirSync(dir, { recursive: true })
|
|
36
|
+
// 직전 status 를 읽어 합치지 않는다 — 플러그인이 없는 필드를 지금 값으로 채운다
|
|
37
|
+
fs.writeFileSync(path.join(dir, sessionId + '.json'), payload.trim(), 'utf8')
|
|
38
|
+
console.log('label -> file')
|
|
39
|
+
} catch (e) {
|
|
40
|
+
console.error('label failed: ' + e.message)
|
|
41
|
+
process.exit(1)
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const sock = net.connect({ port, host: '127.0.0.1' }, () => {
|
|
46
|
+
sock.end(payload, () => {
|
|
47
|
+
console.log('label -> tcp:' + port)
|
|
48
|
+
process.exit(0)
|
|
49
|
+
})
|
|
50
|
+
})
|
|
51
|
+
sock.setTimeout(700)
|
|
52
|
+
sock.on('timeout', () => { sock.destroy(); fallback() })
|
|
53
|
+
sock.on('error', () => fallback())
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
<#
|
|
2
|
+
.SYNOPSIS
|
|
3
|
+
Claude Code 훅 -> AgentDeck 사이드바 상태/작업이름 통보.
|
|
4
|
+
|
|
5
|
+
.DESCRIPTION
|
|
6
|
+
`%LOCALAPPDATA%\tabby-agentdeck\status\<session_id>.json` 에 상태를 쓴다.
|
|
7
|
+
Tabby 쪽 WorkNotifyService 가 이 폴더를 400ms 마다 읽어 사이드바에 반영한다.
|
|
8
|
+
|
|
9
|
+
왜 파일인가 — 콘솔 제목 방식(agentdeck-status.ps1)은 훅에서는 통하지 않는다.
|
|
10
|
+
Claude Code 가 훅을 파이프 stdio 로 띄우므로 그 프로세스는 터미널 콘솔에 붙어
|
|
11
|
+
있지 않고, 제목을 바꿔도 pty 까지 전달되지 않는다 (2026-08-28 실측).
|
|
12
|
+
|
|
13
|
+
라벨은 stdin 으로 들어온 훅 JSON 의 prompt 를 쓴다. prompt 가 없는 훅
|
|
14
|
+
(Stop / Notification) 이면 기존 파일의 라벨을 그대로 유지한다.
|
|
15
|
+
|
|
16
|
+
stdout 으로 아무것도 내보내지 않는다 — UserPromptSubmit 훅의 stdout 은
|
|
17
|
+
프롬프트 컨텍스트로 주입되기 때문이다.
|
|
18
|
+
|
|
19
|
+
.PARAMETER Status
|
|
20
|
+
running(진행중) / waiting(승인대기) / done(완료) / error(오류) / idle(대기)
|
|
21
|
+
|
|
22
|
+
.EXAMPLE
|
|
23
|
+
# settings.json
|
|
24
|
+
# "UserPromptSubmit": powershell -NoProfile -File agentdeck-notify.ps1 -Status running
|
|
25
|
+
# "Stop": powershell -NoProfile -File agentdeck-notify.ps1 -Status done
|
|
26
|
+
#>
|
|
27
|
+
param(
|
|
28
|
+
[ValidateSet('running', 'waiting', 'done', 'error', 'idle')]
|
|
29
|
+
[string]$Status,
|
|
30
|
+
|
|
31
|
+
# Claude 가 직접 정한 작업 이름. 이게 있으면 무엇보다 우선한다
|
|
32
|
+
[string]$Label,
|
|
33
|
+
|
|
34
|
+
# stdin 훅 JSON 없이 부를 때 대상 세션 (없으면 가장 최근에 갱신된 세션)
|
|
35
|
+
[string]$SessionId,
|
|
36
|
+
|
|
37
|
+
# UserPromptSubmit 훅 전용 — 라벨이 아직 비어 있을 때만 프롬프트로 채운다
|
|
38
|
+
[switch]$PromptAsLabel,
|
|
39
|
+
|
|
40
|
+
[int]$MaxLabel = 40
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
$ErrorActionPreference = 'SilentlyContinue'
|
|
44
|
+
|
|
45
|
+
try {
|
|
46
|
+
# PS 5.1 은 stdin 을 시스템 ANSI(CP949) 로 읽어 UTF-8 JSON 의 한글을 깨뜨린다.
|
|
47
|
+
# 표준입력 스트림을 UTF-8 로 직접 열어서 읽는다
|
|
48
|
+
$reader = New-Object System.IO.StreamReader([Console]::OpenStandardInput(), (New-Object System.Text.UTF8Encoding($false)))
|
|
49
|
+
$raw = $reader.ReadToEnd()
|
|
50
|
+
$reader.Close()
|
|
51
|
+
$hook = if ($raw) { $raw | ConvertFrom-Json } else { $null }
|
|
52
|
+
} catch {
|
|
53
|
+
$hook = $null
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
$dir = Join-Path $env:LOCALAPPDATA 'tabby-agentdeck\status'
|
|
57
|
+
if (-not (Test-Path $dir)) { New-Item -ItemType Directory -Force -Path $dir | Out-Null }
|
|
58
|
+
|
|
59
|
+
# 세션 결정 — 훅 JSON > -SessionId > 가장 최근에 갱신된 파일
|
|
60
|
+
$targetId = ''
|
|
61
|
+
if ($hook -and $hook.session_id) {
|
|
62
|
+
$targetId = [string]$hook.session_id
|
|
63
|
+
} elseif ($SessionId) {
|
|
64
|
+
$targetId = $SessionId
|
|
65
|
+
} else {
|
|
66
|
+
$recent = Get-ChildItem -Path $dir -Filter '*.json' -ErrorAction SilentlyContinue |
|
|
67
|
+
Sort-Object LastWriteTime -Descending | Select-Object -First 1
|
|
68
|
+
if ($recent) { $targetId = [System.IO.Path]::GetFileNameWithoutExtension($recent.Name) }
|
|
69
|
+
}
|
|
70
|
+
if (-not $targetId) { $targetId = 'default' }
|
|
71
|
+
# 파일명에 못 쓰는 문자 제거
|
|
72
|
+
$safeId = ($targetId -replace '[^A-Za-z0-9._-]', '_')
|
|
73
|
+
$file = Join-Path $dir "$safeId.json"
|
|
74
|
+
|
|
75
|
+
# 직전 상태를 읽어 둔다 — 라벨 유지와 승인대기 오탐 방지에 쓴다
|
|
76
|
+
$prevStatus = ''
|
|
77
|
+
$prevLabel = ''
|
|
78
|
+
if (Test-Path $file) {
|
|
79
|
+
try {
|
|
80
|
+
$prev = Get-Content -Raw -Encoding UTF8 $file | ConvertFrom-Json
|
|
81
|
+
$prevStatus = [string]$prev.status
|
|
82
|
+
$prevLabel = [string]$prev.label
|
|
83
|
+
} catch { }
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
# --- 승인대기 오탐 차단 ---
|
|
87
|
+
# Notification 훅은 "도구 승인 요청" 말고 "입력이 없다"는 유휴 알림에도 발화한다.
|
|
88
|
+
# 그래서 Stop(done) 뒤에 유휴 알림이 오면 완료된 탭이 '승인대기' 로 뒤집힌다 (2026-08-28 실측:
|
|
89
|
+
# status 폴더의 두 세션이 작업을 끝냈는데도 waiting 으로 박혀 있었다).
|
|
90
|
+
# 승인 요청이 아닌 알림이면 상태를 건드리지 않는다.
|
|
91
|
+
if ($Status -eq 'waiting') {
|
|
92
|
+
$msg = if ($hook -and $hook.message) { [string]$hook.message } else { '' }
|
|
93
|
+
$idleNotice = $msg -match '(?i)waiting for your input|idle'
|
|
94
|
+
$alreadyFinished = $prevStatus -eq 'done' -or $prevStatus -eq 'idle'
|
|
95
|
+
if ($idleNotice -or $alreadyFinished) { exit 0 }
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
# --- 라벨 결정 ---
|
|
99
|
+
# 우선순위: 명시 -Label > 기존 라벨 유지 > (PromptAsLabel 이고 기존 라벨이 없을 때만) 프롬프트
|
|
100
|
+
#
|
|
101
|
+
# 프롬프트를 무조건 라벨로 쓰지 않는 이유: 사용자가 친 문장이 그대로 작업 이름으로 박혀서
|
|
102
|
+
# ("승인대기가 아닌데 왜 승인대기로 나오냐?" 같은 게 라벨이 됐다) 무슨 작업인지 알 수 없게 된다.
|
|
103
|
+
# 작업 이름은 Claude 가 진행도에 맞춰 -Label 로 갱신하는 것이 정상 경로이고,
|
|
104
|
+
# 프롬프트는 그게 아직 한 번도 안 왔을 때의 첫 화면용 폴백일 뿐이다.
|
|
105
|
+
$newLabel = $prevLabel
|
|
106
|
+
if ($Label) {
|
|
107
|
+
$newLabel = $Label
|
|
108
|
+
} elseif ($PromptAsLabel -and -not $prevLabel -and $hook -and $hook.prompt) {
|
|
109
|
+
$newLabel = ([string]$hook.prompt -split "`r?`n" | Where-Object { $_.Trim() } | Select-Object -First 1)
|
|
110
|
+
if ($null -eq $newLabel) { $newLabel = '' }
|
|
111
|
+
}
|
|
112
|
+
$newLabel = ($newLabel -replace '\s+', ' ').Trim()
|
|
113
|
+
if ($newLabel.Length -gt $MaxLabel) { $newLabel = $newLabel.Substring(0, $MaxLabel).TrimEnd() + '...' }
|
|
114
|
+
|
|
115
|
+
# -Status 없이 -Label 만 주면 상태는 그대로 두고 이름만 바꾼다
|
|
116
|
+
$effectiveStatus = if ($Status) { $Status } elseif ($prevStatus) { $prevStatus } else { 'running' }
|
|
117
|
+
|
|
118
|
+
$payload = [ordered]@{
|
|
119
|
+
sessionId = $targetId
|
|
120
|
+
status = $effectiveStatus
|
|
121
|
+
label = $newLabel
|
|
122
|
+
ts = [int64](([datetime]::UtcNow - [datetime]'1970-01-01').TotalMilliseconds)
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
# BOM 없는 UTF-8 로 원자적 쓰기 — Node 의 JSON.parse 는 BOM 을 못 먹는다
|
|
126
|
+
#
|
|
127
|
+
# TCP 로 밀어 넣더라도 이 파일 쓰기는 건너뛰지 않는다 — 파일이 곧 상태 저장소라서
|
|
128
|
+
# 다음 훅 호출이 여기서 직전 상태/라벨을 읽는다(위 $prevStatus / $prevLabel).
|
|
129
|
+
# 그게 라벨 유지와 '승인대기 오탐 차단' 의 근거다.
|
|
130
|
+
$json = $payload | ConvertTo-Json -Compress
|
|
131
|
+
$tmp = "$file.tmp"
|
|
132
|
+
$enc = New-Object System.Text.UTF8Encoding($false)
|
|
133
|
+
[System.IO.File]::WriteAllText($tmp, $json, $enc)
|
|
134
|
+
[System.IO.File]::Copy($tmp, $file, $true)
|
|
135
|
+
Remove-Item $tmp -Force
|
|
136
|
+
|
|
137
|
+
# --- TCP 즉시 통보 ---
|
|
138
|
+
# Tabby 의 WorkNotifyService 가 127.0.0.1 임의 포트로 듣고 그 번호를 port 파일에 적어 둔다.
|
|
139
|
+
# 여기로 JSON 한 줄을 보내면 폴링을 기다리지 않고 사이드바가 바로 바뀐다.
|
|
140
|
+
# 실패는 전부 무시한다 — Tabby 가 안 떠 있거나 포트가 바뀐 것뿐이고, 위에 쓴 파일을 폴링이 주워 간다.
|
|
141
|
+
$portFile = Join-Path $env:LOCALAPPDATA 'tabby-agentdeck\port'
|
|
142
|
+
if (Test-Path $portFile) {
|
|
143
|
+
try {
|
|
144
|
+
$port = [int]((Get-Content -Raw $portFile).Trim())
|
|
145
|
+
if ($port -gt 0) {
|
|
146
|
+
$client = New-Object System.Net.Sockets.TcpClient
|
|
147
|
+
# 훅은 매 이벤트마다 도는 경로다 — Tabby 가 없을 때 여기서 붙잡히면 안 된다
|
|
148
|
+
if ($client.ConnectAsync('127.0.0.1', $port).Wait(200)) {
|
|
149
|
+
$bytes = $enc.GetBytes($json + "`n")
|
|
150
|
+
$stream = $client.GetStream()
|
|
151
|
+
$stream.Write($bytes, 0, $bytes.Length)
|
|
152
|
+
$stream.Flush()
|
|
153
|
+
$stream.Close()
|
|
154
|
+
}
|
|
155
|
+
$client.Close()
|
|
156
|
+
}
|
|
157
|
+
} catch { }
|
|
158
|
+
}
|
|
159
|
+
exit 0
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
<#
|
|
2
|
+
.SYNOPSIS
|
|
3
|
+
Claude Code 훅에서 AgentDeck 사이드바에 작업 상태를 통보한다.
|
|
4
|
+
|
|
5
|
+
.DESCRIPTION
|
|
6
|
+
콘솔 제목을 `[AD:<status>] <라벨>` 형태로 바꾸면 AgentDeck 플러그인이 그것을 읽어
|
|
7
|
+
사이드바 배지/라벨에 반영한다.
|
|
8
|
+
|
|
9
|
+
왜 제목인가 — Windows 의 ConPTY 는 자기가 모르는 OSC 이스케이프를 삼켜서
|
|
10
|
+
커스텀 시퀀스가 터미널까지 오지 못한다(2026-08-28 실측). 반면 콘솔 제목은
|
|
11
|
+
ConPTY 가 API 로 변환해 전달하므로 확실히 통과한다.
|
|
12
|
+
|
|
13
|
+
.PARAMETER Status
|
|
14
|
+
running(진행중) / waiting(승인대기) / done(완료) / error(오류) / idle(대기)
|
|
15
|
+
|
|
16
|
+
.PARAMETER Label
|
|
17
|
+
사이드바에 함께 보일 작업 이름.
|
|
18
|
+
|
|
19
|
+
.EXAMPLE
|
|
20
|
+
powershell -NoProfile -File agentdeck-status.ps1 -Status running -Label "결제 버그 수정"
|
|
21
|
+
powershell -NoProfile -File agentdeck-status.ps1 -Status waiting -Label "배포 승인"
|
|
22
|
+
#>
|
|
23
|
+
param(
|
|
24
|
+
[Parameter(Mandatory = $true)]
|
|
25
|
+
[ValidateSet('running', 'waiting', 'done', 'error', 'idle')]
|
|
26
|
+
[string]$Status,
|
|
27
|
+
|
|
28
|
+
[string]$Label = ''
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
$title = if ($Label) { "[AD:$Status] $Label" } else { "[AD:$Status]" }
|
|
32
|
+
$host.UI.RawUI.WindowTitle = $title
|
|
33
|
+
Write-Output $title
|
package/install.ps1
ADDED
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
<#
|
|
2
|
+
.SYNOPSIS
|
|
3
|
+
AgentDeck 플러그인을 Tabby 에 연결하고, 관리자 권한 실행 바로가기를 만든다.
|
|
4
|
+
|
|
5
|
+
.DESCRIPTION
|
|
6
|
+
Tabby 는 사용자 플러그인 디렉토리(userData/plugins/node_modules)를 스캔하므로
|
|
7
|
+
이 스크립트는 그곳에 프로젝트 폴더를 가리키는 junction 을 만든다.
|
|
8
|
+
소스를 고치고 `npm run build` 만 하면 Tabby 재시작으로 바로 반영된다.
|
|
9
|
+
|
|
10
|
+
.PARAMETER Shortcut
|
|
11
|
+
바탕화면에 "Tabby (AgentDeck)" 관리자 권한 바로가기를 만든다.
|
|
12
|
+
Tabby 를 관리자로 띄우면 그 안에서 열리는 모든 탭이 권한을 물려받으므로
|
|
13
|
+
탭마다 UAC 창이 뜨는 것을 피할 수 있다.
|
|
14
|
+
|
|
15
|
+
.EXAMPLE
|
|
16
|
+
powershell -NoProfile -ExecutionPolicy Bypass -File install.ps1 -Shortcut
|
|
17
|
+
#>
|
|
18
|
+
param(
|
|
19
|
+
[switch]$Shortcut
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
$ErrorActionPreference = 'Stop'
|
|
23
|
+
$source = $PSScriptRoot
|
|
24
|
+
$pluginRoot = Join-Path $env:APPDATA 'tabby\plugins\node_modules'
|
|
25
|
+
$link = Join-Path $pluginRoot 'tabby-agentdeck'
|
|
26
|
+
|
|
27
|
+
# 1) 빌드 산출물 확인
|
|
28
|
+
if (-not (Test-Path (Join-Path $source 'dist\index.js'))) {
|
|
29
|
+
Write-Warning "dist\index.js 가 없다. 먼저 'npm install' 후 'npm run build' 를 실행할 것."
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
# 2) 플러그인 디렉토리에 junction 연결
|
|
33
|
+
New-Item -ItemType Directory -Force -Path $pluginRoot | Out-Null
|
|
34
|
+
if (Test-Path $link) {
|
|
35
|
+
Remove-Item $link -Force -Recurse
|
|
36
|
+
}
|
|
37
|
+
New-Item -ItemType Junction -Path $link -Target $source | Out-Null
|
|
38
|
+
Write-Host "연결됨: $link -> $source"
|
|
39
|
+
|
|
40
|
+
# 2-1) plugins\package.json 에 file: 의존성으로 등록
|
|
41
|
+
# Tabby 플러그인 매니저가 다른 플러그인을 설치하면 npm install 이 돌면서
|
|
42
|
+
# package.json 에 없는 node_modules 항목을 extraneous 로 보고 지운다.
|
|
43
|
+
# 여기 등록해두면 junction 이 정식 의존성이 되어 살아남는다.
|
|
44
|
+
$pkgPath = Join-Path (Split-Path $pluginRoot) 'package.json'
|
|
45
|
+
if (Test-Path $pkgPath) {
|
|
46
|
+
$pkg = Get-Content $pkgPath -Raw | ConvertFrom-Json
|
|
47
|
+
} else {
|
|
48
|
+
$pkg = [pscustomobject]@{ dependencies = [pscustomobject]@{} }
|
|
49
|
+
}
|
|
50
|
+
if (-not $pkg.dependencies) {
|
|
51
|
+
$pkg | Add-Member -NotePropertyName dependencies -NotePropertyValue ([pscustomobject]@{}) -Force
|
|
52
|
+
}
|
|
53
|
+
$spec = 'file:' + ($source -replace '\\', '/')
|
|
54
|
+
$pkg.dependencies | Add-Member -NotePropertyName 'tabby-agentdeck' -NotePropertyValue $spec -Force
|
|
55
|
+
$pkg | ConvertTo-Json -Depth 10 | Out-File $pkgPath -Encoding utf8
|
|
56
|
+
Write-Host "의존성 등록: $pkgPath (tabby-agentdeck = $spec)"
|
|
57
|
+
|
|
58
|
+
# 3) 관리자 권한 바로가기
|
|
59
|
+
if ($Shortcut) {
|
|
60
|
+
$exe = Join-Path $env:LOCALAPPDATA 'Programs\Tabby\Tabby.exe'
|
|
61
|
+
if (-not (Test-Path $exe)) {
|
|
62
|
+
throw "Tabby.exe 를 찾지 못했다: $exe"
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
$lnkPath = Join-Path ([Environment]::GetFolderPath('Desktop')) 'Tabby (AgentDeck).lnk'
|
|
66
|
+
$shell = New-Object -ComObject WScript.Shell
|
|
67
|
+
$lnk = $shell.CreateShortcut($lnkPath)
|
|
68
|
+
$lnk.TargetPath = $exe
|
|
69
|
+
$lnk.WorkingDirectory = Split-Path $exe
|
|
70
|
+
$lnk.Description = 'Tabby + AgentDeck (관리자 권한)'
|
|
71
|
+
$lnk.Save()
|
|
72
|
+
|
|
73
|
+
# 바로가기에 "관리자 권한으로 실행" 플래그를 세운다 (.lnk 바이너리의 21번째 바이트 0x20 비트)
|
|
74
|
+
$bytes = [System.IO.File]::ReadAllBytes($lnkPath)
|
|
75
|
+
$bytes[0x15] = $bytes[0x15] -bor 0x20
|
|
76
|
+
[System.IO.File]::WriteAllBytes($lnkPath, $bytes)
|
|
77
|
+
|
|
78
|
+
Write-Host "바로가기 생성: $lnkPath (관리자 권한)"
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
Write-Host ""
|
|
82
|
+
Write-Host "Tabby 를 재시작하면 적용된다."
|
package/package.json
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "tabby-agentdeck",
|
|
3
|
+
"version": "0.1.0",
|
|
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
|
+
"keywords": [
|
|
6
|
+
"tabby-plugin",
|
|
7
|
+
"tabby",
|
|
8
|
+
"ai",
|
|
9
|
+
"agent",
|
|
10
|
+
"claude-code",
|
|
11
|
+
"codex",
|
|
12
|
+
"terminal"
|
|
13
|
+
],
|
|
14
|
+
"main": "dist/index.js",
|
|
15
|
+
"typings": "dist/index.d.ts",
|
|
16
|
+
"scripts": {
|
|
17
|
+
"build": "webpack --progress --color",
|
|
18
|
+
"watch": "webpack --progress --color --watch",
|
|
19
|
+
"test": "tsc src/prompt.ts --outDir .tmp --module commonjs --target es2019 && node test/prompt.test.js"
|
|
20
|
+
},
|
|
21
|
+
"files": [
|
|
22
|
+
"dist",
|
|
23
|
+
"hooks",
|
|
24
|
+
"install.ps1"
|
|
25
|
+
],
|
|
26
|
+
"license": "MIT",
|
|
27
|
+
"devDependencies": {
|
|
28
|
+
"@angular/common": "^15.2.6",
|
|
29
|
+
"@angular/core": "^15.2.6",
|
|
30
|
+
"@angular/forms": "^15.2.6",
|
|
31
|
+
"@types/node": "^20.19.43",
|
|
32
|
+
"@types/webpack-env": "^1.18.4",
|
|
33
|
+
"css-loader": "^6.10.0",
|
|
34
|
+
"rxjs": "^7.5.7",
|
|
35
|
+
"sass": "^1.71.1",
|
|
36
|
+
"sass-loader": "^14.1.1",
|
|
37
|
+
"style-loader": "^3.3.4",
|
|
38
|
+
"tabby-core": "^1.0.231-nightly.0",
|
|
39
|
+
"tabby-local": "^1.0.231-nightly.0",
|
|
40
|
+
"tabby-settings": "^1.0.231-nightly.0",
|
|
41
|
+
"tabby-terminal": "^1.0.231-nightly.0",
|
|
42
|
+
"ts-loader": "^9.5.1",
|
|
43
|
+
"typescript": "^4.9.5",
|
|
44
|
+
"webpack": "^5.90.0",
|
|
45
|
+
"webpack-cli": "^5.1.4"
|
|
46
|
+
},
|
|
47
|
+
"author": {
|
|
48
|
+
"name": "junggon"
|
|
49
|
+
}
|
|
50
|
+
}
|