tabby-agentdeck 0.3.0 → 1.0.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.
@@ -0,0 +1,120 @@
1
+ /**
2
+ * 세션이 지금 **서브에이전트를 몇 개 돌리고 있나** — Claude Code 대화기록(JSONL) 줄만 보고 세는 순수 로직.
3
+ *
4
+ * 왜 대화기록인가 — 훅(`hooks/agentdeck-notify.ps1`)이 알려 주는 것은 세션 하나의 상태
5
+ * (`running`/`waiting`/…)뿐이다. 그 안에서 백그라운드 Agent 가 몇 개 도는지는 훅에 없고,
6
+ * 대화기록에만 남는다. 그리고 탭 ↔ 세션 연결은 이미 `notify.service.ts` 가 갖고 있으므로
7
+ * (status 파일의 `sessionId`·`tabId`) 세션 id 로 대화기록을 찾아 세면 사이드바에 붙일 수 있다.
8
+ *
9
+ * **개수는 id 대조로 나온다** (실측 2026-09-09, 6,395줄/13MB 세션에서 호출 28 · 도는 중 1 = 실제와 일치):
10
+ * - 호출 = `type:"assistant"` 줄의 `message.content[]` 안 `{type:"tool_use", name:"Agent", id:"toolu_…"}`
11
+ * - 종료 = 어딘가의 문자열에 실린 `<task-notification>` 블록의 `<tool-use-id>`
12
+ * - 도는 중 = 호출된 id 중 종료 알림에 안 나온 것
13
+ *
14
+ * **`tool_result` 를 종료로 보면 안 된다.** 백그라운드 Agent 의 `tool_result` 는 호출 **바로 다음 줄**에
15
+ * `Async agent launched successfully.` 로 온다(실측: 28건 전부 호출줄 +1). 이걸 완료로 세면 항상 0개가 된다.
16
+ *
17
+ * **종료 알림은 JSON 모양이 한 가지가 아니다.** 같은 알림이 실측에서 세 모양으로 나왔다 —
18
+ * ① `type:"user"` 줄의 `message.content` (문자열 그대로)
19
+ * ② `type:"queue-operation"` 줄의 최상위 `content`
20
+ * ③ `type:"attachment"` 줄의 `attachment.prompt`
21
+ * 그래서 알림은 **줄 원문에서 태그를 직접 긁는다.** JSON 모양을 따라가면 셋 중 하나만 잡게 되는데,
22
+ * 처음에 ①만 봤을 때 28건 중 16건을 놓쳤다(도는 중 16 = 오답). 태그는 순수 ASCII 라 JSON 이스케이프가
23
+ * 끼어도(`\n`) 그대로 읽힌다.
24
+ *
25
+ * 파일을 읽거나 경로를 뒤지지 않는다 — 줄(또는 청크)을 받아 상태를 갱신하는 것만 한다.
26
+ * `group.ts`·`gitDiff.ts` 와 같은 이유다: fs·타이머가 들어오면 `npm test` 에서 돌릴 수 없고,
27
+ * 폴링 주기·증분 오프셋 관리는 호출부(서비스)의 일이다.
28
+ */
29
+ /**
30
+ * 서브에이전트를 띄우는 도구 이름. `Task` 는 옛 이름(구버전 CLI)이라 같이 받는다 —
31
+ * 최근 기록에는 `Agent` 만 나오지만, 플러그인은 사용자가 깔아 둔 아무 버전과도 붙는다.
32
+ */
33
+ export declare const AGENT_TOOL_NAMES: readonly string[];
34
+ /** 서브에이전트 호출 한 건 */
35
+ export interface SubagentCall {
36
+ /** `tool_use` 의 id (`toolu_…`) — 종료 알림과 맞추는 열쇠 */
37
+ id: string;
38
+ /** `input.description` — 3~5 낱말짜리 표시용 라벨. 없으면 빈 문자열 */
39
+ description: string;
40
+ /** `input.subagent_type` — 없으면 null (기본 에이전트) */
41
+ subagentType: string | null;
42
+ /** 호출 줄의 `timestamp` (ISO 문자열). 줄에 없으면 null */
43
+ startedAt: string | null;
44
+ }
45
+ /**
46
+ * 증분 스캔 누적 상태.
47
+ *
48
+ * **왜 이 모양인가** — 13MB 까지 자라는 파일을 매번 다 파싱할 수 없어서(실측 6,395줄) 호출부가
49
+ * "지난번 오프셋 이후" 만 읽어 넘기고, 이 객체가 그 사이의 기억을 들고 있어야 한다. 들고 있을 것은
50
+ * 정확히 두 집합이다 —
51
+ * - `calls`: 호출 id → 정보. **Map 인 이유는 삽입 순서**다. 툴팁에 호출된 순서(= 시간순)로 적어야
52
+ * 사람이 읽을 수 있고, Set + 별도 배열로 나누면 두 자료구조가 어긋날 자리가 생긴다.
53
+ * - `done`: 종료 알림을 받은 id. **호출을 아직 못 봤어도 넣는다** — 증분 경계에서 알림이 호출보다
54
+ * 먼저 도착하고(청크가 알림 줄에서 시작), 세션을 이어받으면 앞 파일의 호출은 아예 못 본다.
55
+ * 그러니 `done` 은 호출 여부와 무관하게 단조증가하는 집합이어야 한다.
56
+ * 두 집합을 따로 두고 **차집합으로 답을 만드는** 구조라 순서가 어떻게 오든 결과가 같다(멱등).
57
+ *
58
+ * 완료된 호출을 `calls` 에서 지우지 않는 이유 — 총 호출 수를 진단(`diag`)에 쓸 수 있고, 규모가
59
+ * 세션당 수십 건이라 지워서 얻을 것이 없다. 지우면 "완료 먼저 → 호출 나중" 을 되살릴 수도 없다.
60
+ */
61
+ export interface SubagentState {
62
+ calls: Map<string, SubagentCall>;
63
+ done: Set<string>;
64
+ /** 개행으로 끝나지 않은 마지막 조각 — 다음 청크 앞에 이어 붙인다 */
65
+ pending: string;
66
+ /** 지금까지 먹은 줄 수 (진단용) */
67
+ lines: number;
68
+ /** JSON 이 아니거나 깨져서 버린 완성된 줄 수 (진단용) */
69
+ skipped: number;
70
+ /** `MAX_PENDING` 을 넘겨 버린 꼬리 수 (진단용 — 0 이 아니면 뭔가 이상하다) */
71
+ dropped: number;
72
+ }
73
+ /** 사이드바가 쓰는 결론 */
74
+ export interface SubagentSummary {
75
+ /** 도는 중인 개수 */
76
+ running: number;
77
+ /** 도는 중인 것들 — 호출된 순서(시간순) */
78
+ items: SubagentCall[];
79
+ /** 이 상태가 본 총 호출 수 */
80
+ calls: number;
81
+ /** 종료 알림을 받은 id 수 (호출을 못 본 것도 포함) */
82
+ done: number;
83
+ }
84
+ export declare function createSubagentState(): SubagentState;
85
+ /**
86
+ * **완성된 줄들**을 먹인다. 상태를 그 자리에서 고치고 같은 객체를 돌려준다
87
+ * (세션당 상태 하나를 계속 들고 도는 쓰임새라 사본을 만들 이유가 없다).
88
+ *
89
+ * **깨진 줄은 버린다.** 개행이 왔다는 것은 그 줄이 다 쓰였다는 뜻이라, 다시 읽어도 결과가 같다.
90
+ * "다음에 다시 보자" 로 남기면 매 폴링마다 같은 줄을 파싱하는 무한 재시도가 된다.
91
+ * 반대로 **개행 없는 꼬리는 남긴다** — 그건 "깨진 것" 이 아니라 "아직 안 온 것" 이고,
92
+ * 게다가 꼬리에 있는 이벤트는 거의 항상 **가장 최신**(방금 띄운 Agent / 방금 온 종료 알림)이라
93
+ * 버리면 증분 구조에서는 되찾을 기회가 없다(파일을 처음부터 다시 읽지 않는다). 그 구분은
94
+ * `feedChunk` 가 개행으로 판정한다.
95
+ */
96
+ export declare function feedLines(state: SubagentState, lines: readonly string[]): SubagentState;
97
+ /**
98
+ * 파일에서 읽은 **청크**를 먹인다. 개행으로 끝나지 않은 마지막 조각은 다음 호출까지 들고 있는다.
99
+ *
100
+ * 호출부는 "지난 크기 이후만 읽기" 만 하면 되고 줄 경계는 여기서 맞춘다 — 그 경계 처리를 호출부마다
101
+ * 다시 쓰면(서비스·진단구) 사본이 갈린다. 빈 청크(파일이 안 자랐다)는 아무 일도 하지 않는다.
102
+ */
103
+ export declare function feedChunk(state: SubagentState, chunk: string): SubagentState;
104
+ /** 도는 중인 것 = 호출된 것 중 종료 알림에 안 나온 것. 호출 순서를 지킨다 */
105
+ export declare function summarizeSubagents(state: SubagentState | null | undefined): SubagentSummary;
106
+ /**
107
+ * 사이드바 title(툴팁)에 넣을 문장. **도는 것이 없으면 빈 문자열** — 호출부가 title 을 아예 안 붙이게.
108
+ *
109
+ * 개수만으로는 "무엇을 시켜 놨는지" 를 모른다(그게 이 기능의 목적이다). 그래서 설명을 줄줄이 붙이는데,
110
+ * 200px 사이드바(config `sidebarMin`)의 툴팁이 화면을 덮지 않게 `max` 개까지만 적고 나머지는 수로 줄인다.
111
+ */
112
+ export declare function formatSubagentTooltip(summary: SubagentSummary | null | undefined, max?: number): string;
113
+ /**
114
+ * 이 파일명이 그 세션의 대화기록인가 — 호출부가 `~/.claude/projects` 의 각 프로젝트 폴더를 훑을 때 쓰는 판정.
115
+ *
116
+ * 프로젝트 폴더 이름 규칙(경로를 `-` 로 뭉갠 이름)에 기대지 않으려고 폴더를 훑는 방식을 쓴다.
117
+ * 규칙을 흉내내면 드라이브·대소문자·특수문자에서 어긋나는데, 세션 id 로 파일명을 맞추면 폴더를
118
+ * 몰라도 정확히 하나가 잡힌다(실측). 대소문자를 무시하는 것은 윈도우 파일명이라서다.
119
+ */
120
+ export declare function matchesSessionTranscript(fileName: string | null | undefined, sessionId: string | null | undefined): boolean;
@@ -0,0 +1,374 @@
1
+ /**
2
+ * 결과물 미리보기 패널 — 터미널 옆에서 md·이미지·표·텍스트를 그대로 본다.
3
+ *
4
+ * 왜 필요한가: 에이전트가 만든 것이 문서(md)나 그림이면 터미널 안에서는 확인이 안 된다.
5
+ * 지금까지는 탐색기로 나가서 열었고, 그 왕복이 "작업 결과를 안 보고 넘어가는" 이유였다.
6
+ * Codex 앱의 오른쪽 패널이 하는 일과 같은 자리다.
7
+ *
8
+ * **어느 에이전트에도 매이지 않는다** — 보여줄 파일을 고르는 근거가 세 갈래인데 셋 다
9
+ * 특정 CLI 의 기능이 아니다. ① 화면에 찍힌 경로를 줍는다(Claude·Codex·Gemini 가 모두 찍는다)
10
+ * ② 사람이 경로를 넣거나 파일을 끌어다 놓는다 ③ 최근 목록에서 고른다.
11
+ *
12
+ * 이 파일이 하는 일은 DOM 과 파일 접근이다. 판정 로직(종류 분류·경로 줍기·최근 목록)은
13
+ * viewer.ts 에, 마크다운 렌더는 markdown.ts 에 순수 함수로 있다 — dock.ts 를 deck.service 에서
14
+ * 가른 것과 같은 이유(테스트로 묶기 위해서)다.
15
+ */
16
+ import { FollowMode } from './viewer';
17
+ export type ViewSide = 'left' | 'right';
18
+ /**
19
+ * 패널이 보여주는 두 갈래.
20
+ * - `file` = 결과물 하나 (md·이미지·표·코드)
21
+ * - `diff` = 작업트리의 변경 전체 (`git diff` 기준 — 어느 에이전트가 고쳤든 성립한다)
22
+ *
23
+ * 설정에 저장하지 않는다 — 어떤 파일을 보고 있었는지도 저장하지 않으므로 모드만 복원하면
24
+ * 기동 직후 빈 `변경` 탭이 뜬다. 시작은 항상 `file` 이다.
25
+ */
26
+ export type ViewMode = 'file' | 'diff';
27
+ export interface ViewHost {
28
+ /** 패널을 얹을 `.window` */
29
+ windowEl: HTMLElement;
30
+ getSide(): ViewSide;
31
+ getWidth(): number;
32
+ setWidth(px: number): void;
33
+ getRecentMax(): number;
34
+ /** 새 파일을 주웠을 때 읽어둘지·스스로 열지 (`viewerPreload` + `viewerAutoOpen`) */
35
+ getFollowMode(): FollowMode;
36
+ /** 상대경로를 풀 기준 디렉토리 — 그 탭의 cwd. 모르면 null */
37
+ cwdFor(tab: object): string | null;
38
+ /** 드래그가 끝났을 때 한 번 — config.save() 를 부르라는 뜻 */
39
+ commit(): void;
40
+ /** 크기·열림 상태가 바뀌었으니 레이아웃을 다시 잡으라는 뜻 */
41
+ relayout(): void;
42
+ /** 열림 상태를 설정에 적는다 (다음 기동에 복원) */
43
+ setOpen(open: boolean): void;
44
+ /** 외부 링크를 기본 브라우저로 */
45
+ openExternal(url: string): void;
46
+ /**
47
+ * 고른 줄의 참조(`파일:라인`)를 **활성 탭 터미널에 넣는다.** 개행은 붙이지 않는다 —
48
+ * 사용자가 앞뒤에 말을 붙일 자리를 남긴다.
49
+ *
50
+ * 돌려주는 값은 "보냈는가" — 보낼 팬이 없으면 false 를 주고, 그 사실은 패널이 문장으로 말한다.
51
+ * **선택(optional)인 이유** — 배선이 아직 없어도 패널은 동작해야 한다(참조만 못 넣는다).
52
+ */
53
+ sendText?(text: string): boolean;
54
+ }
55
+ export declare const VIEW_ID = "agentdeck-view";
56
+ /** 패널이 이보다 좁으면 문서를 읽을 수 없다 */
57
+ export declare const MIN_VIEW_W = 260;
58
+ export declare class ViewPanel {
59
+ private host;
60
+ private el;
61
+ private bodyEl;
62
+ private tabsEl;
63
+ private titleEl;
64
+ private footEl;
65
+ private pathRow;
66
+ private pathInput;
67
+ private modesEl;
68
+ private commitEl;
69
+ private commitInput;
70
+ private commitBtn;
71
+ private commitSumEl;
72
+ private commitNoteEl;
73
+ private refNoteEl;
74
+ /**
75
+ * 지금 고른 줄 (`파일` 은 절대경로, `변경` 은 저장소 루트 기준 경로 — DOM 의
76
+ * `data-ad-ref-file` 값과 같은 문자열이다).
77
+ *
78
+ * **본문이 아니라 클래스 필드에 둔다.** `변경` 본문은 자동 갱신(DIFF_AUTO_MS)마다
79
+ * innerHTML 을 다시 쓰므로 화면에 붙여 둔 강조(`ad-ref-sel`)는 그때 통째로 사라진다.
80
+ * 여기에 두면 렌더 끝에 `applyRefSel()` 이 같은 줄에 클래스를 다시 붙일 수 있다 —
81
+ * 커밋·찾기 줄을 본문 밖에 둔 것과 같은 사정이다.
82
+ */
83
+ private refSel;
84
+ /**
85
+ * 마지막으로 터미널에 넣은 참조와 그 시각. 다음 참조가 이것의 접두면 **늘어난 꼬리만**
86
+ * 보내 프롬프트에 `a.ts:12a.ts:12-20` 이 쌓이는 것을 막는다 (sendRef 참조).
87
+ */
88
+ private refLastSent;
89
+ /** 안내줄의 강조를 떼는 타이머 (REF_FLASH_MS) */
90
+ private refFlashTimer;
91
+ private mode;
92
+ /**
93
+ * 지금 기다리는 git 호출의 번호. 응답이 돌아왔을 때 이 값이 바뀌어 있으면 버린다 —
94
+ * 탭을 옮기거나 ⟳ 를 연달아 누르면 늦게 온 응답이 새 화면을 덮는다.
95
+ */
96
+ private diffSeq;
97
+ /** 마지막 자동 갱신 시각 (ms) — DIFF_AUTO_MS 간격 제한용 */
98
+ private diffAutoAt;
99
+ /** `변경` 본문의 경로를 눌렀을 때 쓸 저장소 루트 (git 이 내는 경로가 이 기준이다) */
100
+ private diffRoot;
101
+ /** 마지막으로 읽은 `git status` — 커밋 줄의 요약과 "올린 게 없다" 판정의 근거 */
102
+ private statusEntries;
103
+ /** 위 목록의 경로 색인 — 파일 목록 한 줄마다 딱지를 붙일 때 찾는다 */
104
+ private statusByPath;
105
+ /**
106
+ * 커밋 버튼이 "확인 대기" 인가. **첫 누름은 확인만, 두 번째 누름이 실제 커밋이다** —
107
+ * 커밋은 되돌리기 번거로운 부작용이라 한 번의 오클릭(또는 자동화의 맹목적 click)으로
108
+ * 만들어지면 안 된다. 메시지를 고치거나 모드를 옮기면 풀린다.
109
+ */
110
+ private commitArmed;
111
+ /** git 이 도는 중 — 스테이지 딱지를 연달아 누르면 add/reset 이 겹쳐 순서가 뒤집힌다 */
112
+ private gitBusy;
113
+ private opened;
114
+ /** 레이아웃 자체가 꺼져 있으면(agentDeck.enabled=false) 열려 있어도 그리지 않는다 */
115
+ private visible;
116
+ /** 지금 보고 있는 파일 (절대경로) */
117
+ private current;
118
+ /** 파일 감시 중인 경로 — 바꿀 때 반드시 해제한다 */
119
+ private watching;
120
+ /** 탭별 최근 파일. 탭이 닫히면 같이 사라지도록 WeakMap */
121
+ private recent;
122
+ /**
123
+ * 탭별로 **마지막에 보던 파일**. 최근 목록의 맨 앞과 다를 수 있다 — 사람이 칩에서 옛 파일을
124
+ * 고르면 보고 있는 것은 그 파일이고 목록 맨 앞은 다른 것이다. 탭을 옮겼다 돌아왔을 때
125
+ * 되돌아갈 자리는 "보던 것" 이지 "가장 최근에 만져진 것" 이 아니다.
126
+ */
127
+ private lastViewed;
128
+ private activeTab;
129
+ /** 탭별 출력 버퍼와 훑기 타이머 */
130
+ /**
131
+ * 편집 중인가. 켜져 있으면 **화면을 남이 건드리지 못한다** — 파일 감시의 자동 재읽기,
132
+ * 새 파일 따라가기(follow), 모드 전환이 모두 비켜 간다. 사람이 친 글자를 지우는 것보다
133
+ * 화면이 잠깐 낡아 보이는 편이 낫다.
134
+ */
135
+ private editing;
136
+ private editArea;
137
+ private editBarEl;
138
+ private editNoteEl;
139
+ private editSaveBtn;
140
+ /** 편집을 연 시점의 원본 — 저장할 때 BOM·개행을 이대로 되돌리고, mtime 으로 외부 변경을 본다 */
141
+ private editOrigin;
142
+ /** 읽기 전용 속성을 풀고 저장할지 확인 대기 — 덮어쓰기 확인과 같은 2단 규칙 */
143
+ private editForceArmed;
144
+ /** `+` 에 없는 경로를 넣었을 때, 만들지 확인 대기 중인 절대경로 */
145
+ private pathCreateArmed;
146
+ /** 편집하는 동안 파일이 밖에서 바뀌었다 — 저장 버튼이 한 번 더 확인을 받는다 */
147
+ private editStale;
148
+ /** 덮어쓰기 확인 대기 (커밋 버튼의 2단 확인과 같은 규칙) */
149
+ private editOverwriteArmed;
150
+ private findRow;
151
+ private findInput;
152
+ private findCountEl;
153
+ /** 지금 표시 중인 일치 `<mark>` 들. 본문을 다시 그리면 통째로 버린다 */
154
+ private findHits;
155
+ private findIndex;
156
+ private buffers;
157
+ private scrapeTimers;
158
+ private resizing;
159
+ constructor(host: ViewHost);
160
+ get isOpen(): boolean;
161
+ /**
162
+ * 지금 화면에 그려질 폭 (닫혀 있거나 숨어 있으면 0).
163
+ * @param room 패널이 써도 되는 폭 — deck 이 터미널·사이드바의 최소치를 뺀 값을 넘겨준다
164
+ */
165
+ extent(room: number): number;
166
+ setOpen(open: boolean, remember?: boolean): void;
167
+ toggle(): void;
168
+ /** 레이아웃 on/off 를 따라간다 — 열림 상태는 건드리지 않는다 */
169
+ setVisible(visible: boolean): void;
170
+ private syncDisplay;
171
+ install(): void;
172
+ /** 파일을 끌어다 놓으면 그걸 연다 — 탐색기·VS Code 탭 어느 쪽에서 끌어도 된다 */
173
+ private installDrop;
174
+ /** 터미널과 닿는 쪽 경계선을 끌어 폭을 바꾼다 (사이드바의 dock.ts 와 같은 조작) */
175
+ private installResize;
176
+ /** deck 의 relayout 이 정해 준 자리에 앉는다 (사이드바와 같은 방식으로 절대 배치) */
177
+ layout(side: ViewSide, width: number, outerOffset: number): void;
178
+ private togglePathRow;
179
+ private setPathNote;
180
+ /**
181
+ * `+` 에 넣은 경로를 연다. **없는 파일이면 만들어 준다** — 메모 한 장을 시작하려고
182
+ * 탐색기로 나가는 것이 이 패널이 없애려던 왕복이다.
183
+ *
184
+ * 만들기는 **두 번째 Enter 에서만** 한다(커밋·덮어쓰기와 같은 2단 규칙). 오타로 친 경로에
185
+ * 빈 파일이 조용히 생기면, 다음에 그 오타를 찾는 데 드는 시간이 아낀 시간보다 크다.
186
+ * 만든 뒤에는 곧바로 편집으로 들어간다 — 빈 파일을 보여 줄 이유가 없다.
187
+ */
188
+ private openOrCreate;
189
+ get viewMode(): ViewMode;
190
+ /** 스위치 모양만 맞춘다 — 그리기는 부르는 쪽이 한다 (두 번 그리지 않기 위해서) */
191
+ private applyMode;
192
+ setMode(mode: ViewMode): void;
193
+ /**
194
+ * `파일` ↔ `변경` 전환. 핫키(`agentdeck-view-mode`) 배선이 한 줄로 끝나게 public 으로 둔다 —
195
+ * 핫키 처리는 deck.service 가 하고(HotkeysService 순환 의존 때문에 provider 에서는 못 한다),
196
+ * 여기서 상태를 들고 있으므로 그쪽은 이 메서드만 부르면 된다.
197
+ */
198
+ toggleMode(): void;
199
+ /**
200
+ * 출력 한 조각을 받아 둔다. 조각마다 정규식을 돌리지 않고 모았다가 400ms 마다 한 번 훑는다 —
201
+ * 에이전트 TUI 는 상태줄을 매초 다시 그려서 조각이 초당 수십 개씩 온다.
202
+ */
203
+ noteOutput(tab: object, data: string): void;
204
+ private scrape;
205
+ /** 후보 토큰을 실제로 있는 파일의 절대경로로 만든다 (없으면 null) */
206
+ private resolve;
207
+ /** 활성 탭이 바뀌면 최근 목록도 그 탭 것으로 바꾼다 */
208
+ setActiveTab(tab: object | null): void;
209
+ /**
210
+ * 활성 탭이 마지막으로 보던 파일로 본문을 맞춘다.
211
+ *
212
+ * **열려 있을 때만 그린다.** 닫혀 있으면 `current` 만 바꿔 두고, `setOpen(true)` 가 다시 불러
213
+ * 그때 그린다 — 탭을 옮긴 것만으로 패널이 열리면 안 되기 때문이다(패널이 저절로 열리는
214
+ * 경우는 "새 결과물을 주웠을 때" 하나로 족하다).
215
+ *
216
+ * 돌려주는 값은 **"그렸는가"** — 부르는 쪽이 두 번 그리지 않으려고 본다 (`setOpen`).
217
+ */
218
+ private applyTabFile;
219
+ /** 사람이/훅이 알려 준 파일을 최근 목록에 얹는다 (열지는 않는다) */
220
+ noteFile(tab: object, file: string): void;
221
+ openFile(file: string): void;
222
+ reload(): void;
223
+ /** 파일이 바뀌면 다시 그린다 — 에이전트가 문서를 고치는 동안 옆에서 따라간다 */
224
+ private watch;
225
+ private unwatch;
226
+ private renderTabs;
227
+ private renderEmpty;
228
+ private renderMessage;
229
+ private setTitle;
230
+ private setFoot;
231
+ /**
232
+ * 발밑 줄 **앞에** 한 마디를 붙인다 (예: `저장했다 · 1.2KB · …`).
233
+ * 저장 직후처럼 "방금 무슨 일이 있었는지" 를 크기·시각과 같이 보여줄 때 쓴다 —
234
+ * 다음 `renderCurrent` 가 발밑 줄을 다시 쓰면 저절로 사라진다.
235
+ */
236
+ private setFootNote;
237
+ private renderCurrent;
238
+ private renderImage;
239
+ private renderMarkdownFile;
240
+ private renderTable;
241
+ private renderText;
242
+ private renderBinary;
243
+ /** 지금 보고 있는 것이 편집할 수 있는 글자 파일인가 (이미지·바이너리·`변경` 탭 제외) */
244
+ private canEdit;
245
+ /** 편집 버튼의 표시 상태를 지금 화면에 맞춘다 */
246
+ private syncEditButton;
247
+ private toggleEdit;
248
+ private startEdit;
249
+ private cancelEdit;
250
+ private saveEdit;
251
+ private showEditBar;
252
+ private setEditNote;
253
+ /** 편집 중 안내줄 — 바뀐 줄 수와 외부 변경을 알린다 */
254
+ private refreshEditNote;
255
+ private openFind;
256
+ private closeFind;
257
+ /** 검색어가 바뀔 때마다 다시 찾는다 */
258
+ private runFind;
259
+ /** 다음/이전 일치로 이동 */
260
+ private stepFind;
261
+ /**
262
+ * textarea 안 찾기 — 커서 위치부터 다음 일치를 골라 **선택**한다.
263
+ * 본문 찾기와 달리 DOM 을 건드리지 않으므로 편집 내용이 흔들리지 않는다.
264
+ */
265
+ private findInTextarea;
266
+ /**
267
+ * 본문 텍스트 노드를 훑어 일치마다 `<mark>` 를 넣는다.
268
+ *
269
+ * 본문을 다시 그리지 않고 감싸기만 하는 이유 — 줄 참조(`.ad-cl`)·표·마크다운이 저마다
270
+ * 다른 구조라, 그리는 쪽을 고치면 네 군데를 같이 고쳐야 한다. 텍스트 노드만 건드리면
271
+ * 어떤 구조에서도 성립하고, 지울 때는 `normalize()` 로 원래대로 돌아간다.
272
+ */
273
+ private markMatches;
274
+ /** `<mark>` 를 걷어내고 원래 텍스트로 되돌린다 */
275
+ private clearFindMarks;
276
+ private showFindHit;
277
+ private setFindCount;
278
+ /**
279
+ * git 한 번. 실패도 값으로 돌려준다 — 저장소가 아니거나 git 이 없는 것은 흔한 상태이고
280
+ * 예외로 던지면 패널이 빈 화면으로 남는다.
281
+ */
282
+ private runGit;
283
+ /** 자동 갱신 — 간격 제한을 지킨다 (에이전트가 연달아 고치면 신호가 초당 몇 번씩 온다) */
284
+ private maybeAutoDiff;
285
+ /**
286
+ * 작업트리의 변경을 다시 읽어 그린다.
287
+ *
288
+ * **`diff HEAD` 를 쓰는 이유** — 스테이지에 올렸는지 여부는 "무엇이 바뀌었나" 와 상관이 없다.
289
+ * 에이전트가 `git add` 를 했든 안 했든 사람이 보려는 것은 같은 것이고, 그냥 `git diff` 는
290
+ * 스테이지에 올라간 변경을 감춰 버린다.
291
+ */
292
+ private refreshDiff;
293
+ /** 늦게 온 응답이 새 화면을 덮지 않게 — 번호와 모드가 그대로일 때만 그린다 */
294
+ private finishDiff;
295
+ /**
296
+ * 실패 문장을 그리고 **스테이지 상태를 비운다.**
297
+ * 상태를 남겨 두면 저장소를 못 읽는 화면에서 딱지·요약이 이전 저장소 것으로 남고,
298
+ * 그 딱지를 누르면 지금 보고 있지 않은 저장소에 `git add` 가 나간다.
299
+ */
300
+ private failDiff;
301
+ private renderDiffNote;
302
+ private renderDiff;
303
+ /**
304
+ * diff 한 줄. **원문은 반드시 이스케이프한 뒤 우리 태그만 붙인다** — 소스코드 안의
305
+ * `<script>` 나 `onerror=` 가 태그가 되면 안 된다 (markdown.ts 와 같은 규칙).
306
+ */
307
+ private diffLineHtml;
308
+ /** git 이 내는 경로는 **저장소 루트 기준**이다 — 탭 cwd 로 풀면 하위 폴더에서 어긋난다 */
309
+ private absOf;
310
+ /**
311
+ * 줄을 눌렀다. Shift 면 직전에 고른 줄부터의 **범위**가 된다.
312
+ *
313
+ * 본문을 다시 그리지 않고 클래스만 갈아 끼운다 — 다시 그리면 스크롤이 튀고(긴 diff 에서
314
+ * 방금 보던 자리를 잃는다) `변경` 은 git 을 한 번 더 돌린다.
315
+ */
316
+ private clickRefLine;
317
+ /**
318
+ * 고른 줄에 `ad-ref-sel` 을 다시 붙인다. 본문을 그린 뒤마다 불린다 —
319
+ * 상태는 `refSel`(클래스 필드)에 있어서 자동 갱신이 본문을 덮어도 선택이 산다.
320
+ *
321
+ * 경로를 CSS 선택자에 넣지 않고 값을 읽어 비교한다 — 경로에는 선택자 특수문자가
322
+ * 들어올 수 있고(공백·괄호·`#`), 이스케이프를 우리가 다시 구현할 이유가 없다.
323
+ */
324
+ private applyRefSel;
325
+ /**
326
+ * 참조 한 낱말을 만들어 터미널에 넣는다.
327
+ *
328
+ * `변경` 은 저장소 루트 기준 경로를, `파일` 은 절대경로를 상자에 심어 둔다 —
329
+ * `joinRefPath` 가 이미 절대경로면 그대로 돌려주므로 두 갈래를 한 줄로 다룰 수 있다.
330
+ * 그 다음 `refPathFrom` 이 **그 탭 cwd 기준**으로 줄이고, `formatLineRef` 가 번호와
331
+ * 따옴표를 맡는다.
332
+ */
333
+ private sendRef;
334
+ /**
335
+ * 참조 안내줄. 방금 일어난 일일 때만(`flash`) 강조를 얹고 REF_FLASH_MS 뒤에 뗀다 —
336
+ * 계속 켜져 있으면 다음에 무엇이 새로 일어났는지 구별이 안 된다.
337
+ */
338
+ private setRefNote;
339
+ /**
340
+ * 파일 한 줄에 붙는 스테이지 딱지.
341
+ *
342
+ * 글자는 **지금 상태**를, `title` 은 누르면 무엇이 되는지를 말한다 — 딱지가 동작을 가리키면
343
+ * "올림" 이 "올렸다" 인지 "올린다" 인지 읽는 사람이 알 수 없다.
344
+ * status 에 없는 경로면 **아무것도 붙이지 않는다.** 상태를 모르는 채로 누를 것을 주면
345
+ * 사람이 의도하지 않은 것이 올라간다.
346
+ */
347
+ private stageChipHtml;
348
+ /**
349
+ * 읽어 온 status 를 기억하고 커밋 줄의 요약·버튼 잠금을 맞춘다.
350
+ * **알림 문장(note)은 건드리지 않는다** — 자동 갱신이 1.5초마다 도는데 여기서 note 를 쓰면
351
+ * 방금 한 조작의 결과("커밋했다" 등)가 곧바로 지워진다.
352
+ */
353
+ private setStatus;
354
+ /** 버튼의 잠금·라벨을 상태에서 다시 계산한다 (상태를 바꾸는 곳마다 이걸 부른다) */
355
+ private syncCommitButton;
356
+ private setCommitNote;
357
+ private disarmCommit;
358
+ /**
359
+ * 스테이지 토글. **사람이 딱지를 누른 경로에서만 불린다** — 자동 갱신(viewerFollow·파일 감시·
360
+ * maybeAutoDiff)은 이 함수를 부르지 않는다.
361
+ *
362
+ * git 은 **저장소 루트**에서 돌린다. status/diff 가 내는 경로가 루트 기준이라 탭 cwd 에서
363
+ * 돌리면 하위 폴더에서 "pathspec 이 없다" 가 된다 (absOf 와 같은 사정).
364
+ */
365
+ private toggleStage;
366
+ /**
367
+ * 커밋 버튼 / 입력창 Enter. **두 번 눌러야 커밋된다** — 첫 누름은 확인 대기(arm),
368
+ * 두 번째 누름이 실행이다. 검증에 걸리면 첫 누름에서 이유만 말하고 끝난다.
369
+ */
370
+ private pressCommit;
371
+ /** 실제 커밋. pressCommit 의 확인 단계를 지난 뒤에만 불린다 */
372
+ private runCommit;
373
+ dispose(): void;
374
+ }
@@ -0,0 +1,100 @@
1
+ /**
2
+ * 미리보기 패널의 순수 로직 — 파일 종류 판정, PTY 출력에서 경로 줍기, 최근 목록 관리.
3
+ *
4
+ * DOM 도 fs 도 건드리지 않는다. 이 파일만 테스트로 묶어 두고(test/viewer.test.js),
5
+ * 화면·파일 접근은 viewPanel.ts 가 맡는다 — dock.ts / deck.service.ts 를 가른 것과 같은 방식이다.
6
+ *
7
+ * **왜 에이전트 훅이 아니라 화면 출력에서 경로를 줍나 —** 훅이 있는 것은 Claude Code 뿐이다
8
+ * (src/claudeHooks.ts). Codex·Gemini 도 같은 패널을 쓰게 하려면 셋이 공통으로 하는 일에
9
+ * 기대야 하고, 그건 "만진 파일 경로를 화면에 찍는다" 다. 그래서 이 경로가 기본이고
10
+ * 훅은 나중에 정확도를 더하는 보강일 뿐이다.
11
+ */
12
+ export type ViewKind = 'markdown' | 'image' | 'table' | 'text' | 'binary';
13
+ export declare function extensionOf(file: string): string;
14
+ export declare function classify(file: string): ViewKind;
15
+ /** 미리보기로 열어 볼 가치가 있는 확장자인가 — PTY 에서 주운 경로를 거르는 데 쓴다 */
16
+ export declare function isKnownExt(file: string): boolean;
17
+ /**
18
+ * ANSI 제어 시퀀스와 TUI 가 그리는 테두리 문자를 걷어낸다.
19
+ *
20
+ * 경로를 주우려면 필요하다 — 에이전트 TUI 는 `│ Edited src/a.ts │` 처럼 테두리 안에 글자를
21
+ * 그리고, 색을 붙이려고 경로 중간에 SGR 을 끼워 넣기도 한다.
22
+ */
23
+ export declare function stripAnsi(data: string): string;
24
+ /**
25
+ * 출력 한 조각에서 파일 경로 후보를 뽑는다.
26
+ *
27
+ * 여기서는 **후보까지만** 낸다 — 실제로 있는 파일인지, 상대경로를 어느 cwd 로 푸는지는
28
+ * 패널이 fs 로 확인한다. 순수 함수로 남겨 두면 규칙을 테스트로 고정할 수 있다.
29
+ *
30
+ * 걸러내는 것: 확장자를 모르는 것 / 잘려서 `…` 이 들어간 것 / URL / 버전 번호처럼 보이는 것.
31
+ */
32
+ export declare function extractPaths(data: string): string[];
33
+ /**
34
+ * 최근 목록에 하나를 얹는다 (같은 것은 위로 올리고, 최대 개수를 넘으면 뒤를 버린다).
35
+ * 원본을 바꾸지 않고 새 배열을 돌려준다 — 렌더가 이전 배열과 비교할 수 있어야 한다.
36
+ */
37
+ export declare function pushRecent(list: readonly string[], file: string, max: number): string[];
38
+ /**
39
+ * CSV/TSV 한 장을 표로 가른다.
40
+ *
41
+ * 따옴표로 감싼 칸(`"a,b"`)과 그 안의 이스케이프(`""`)까지만 본다 — 기획데이터를 편집하는
42
+ * 도구가 아니라 눈으로 확인하는 미리보기라서 그 이상은 필요하지 않다.
43
+ */
44
+ export declare function parseDelimited(text: string, delimiter?: string): string[][];
45
+ /** 확장자로 구분자를 고른다 */
46
+ export declare function delimiterFor(file: string): string;
47
+ export declare function formatBytes(bytes: number): string;
48
+ /**
49
+ * 새 파일을 주웠을 때 패널이 스스로 무엇을 할지.
50
+ *
51
+ * - `open` : 닫혀 있으면 **열면서** 띄운다
52
+ * - `show` : 읽어만 둔다 — 열려 있으면 그 자리에 띄우고, 닫혀 있으면 **다음에 열 때** 보이게
53
+ * 대상만 갈아 둔다 (기본). 스스로 열지는 않는다
54
+ * - `manual` : 아무것도 하지 않는다. 경로는 최근 칩에만 쌓이고 여닫기·고르기는 사람 몫이다
55
+ *
56
+ * 사용자에게는 이게 목록이 아니라 **토글 두 개**로 보인다 (`viewerPreload`/`viewerAutoOpen`) —
57
+ * `followModeOf` 가 그 둘을 여기로 접는다. 안쪽을 셋으로 유지하는 이유는 판정이 세 갈래이고
58
+ * (`planFollow`) 테스트가 그 표를 계약으로 박아 두었기 때문이다.
59
+ */
60
+ export type FollowMode = 'open' | 'show' | 'manual';
61
+ /**
62
+ * 토글 두 개를 모드로 접는다.
63
+ *
64
+ * - 읽어두기 끔 → `manual` (자동으로 열기는 의미가 없어진다)
65
+ * - 읽어두기 + 자동으로 열기 → `open`
66
+ * - 읽어두기만 → `show` (기본)
67
+ *
68
+ * 두 축으로 가른 이유 — 목록 하나로는 "내용을 채워 둘까" 와 "화면을 빼앗아도 될까" 를 한 번에
69
+ * 골라야 했다. 사람이 실제로 정하고 싶은 것은 그 둘이 따로다 (2026-09-10 유저 결정:
70
+ * "읽어두기는 켜고, 자동으로 열기는 꺼라 — 내가 열면 그때까지 만진 게 보이면 된다").
71
+ */
72
+ export declare function followModeOf(preload: unknown, autoOpen: unknown): FollowMode;
73
+ /**
74
+ * 옛 설정을 토글 두 개로 옮긴다 — 기동 때 **한 번만** 돈다 (`migrateFollowConfig`).
75
+ *
76
+ * 고른 적이 없으면 `null` 을 주고, 그러면 새 기본값(읽어두기 ON / 자동열기 OFF)이 그대로 쓰인다.
77
+ * 읽는 시점에 옛 값을 보는 방식(0.17.0 의 `followMode(raw, legacy)`)을 버린 이유는, 토글은
78
+ * boolean 이라 "아직 안 골랐다" 를 표현할 자리가 없어서다 — 빈 값을 두면 토글이 꺼져 보이는데
79
+ * 실제 동작은 켜져 있는 상태가 된다. 그래서 한 번 적고 옛 값을 비운다.
80
+ */
81
+ export declare function migrateFollow(raw: unknown, legacy?: unknown): {
82
+ preload: boolean;
83
+ autoOpen: boolean;
84
+ } | null;
85
+ /**
86
+ * 새 파일을 주웠다 — 지금 무엇을 할까.
87
+ *
88
+ * - `none` : 아무것도 하지 않는다 (칩만 쌓인다)
89
+ * - `show` : 이미 열려 있으니 그 자리에 띄운다
90
+ * - `open` : 닫혀 있으니 열면서 띄운다
91
+ * - `stage` : 닫혀 있다 — 그리지 않고 **대상만 갈아 둔다.** 사람이 여는 순간 이 파일이 보인다
92
+ *
93
+ * `stage` 를 둔 이유 — 예전에는 이 자리가 `none` 이라, 닫아 두고 일하다 패널을 열면 **아까
94
+ * 보던 옛 파일**이 떠 있었다(`lastViewed` 가 그때 멈춰 있었다). "작업 막 진행하다가 패널을
95
+ * 열면 지금까지 건드린 것이 보여야 한다" 가 이 패널의 요점이므로, 열지는 않되 읽어는 둔다.
96
+ *
97
+ * **편집 중에는 무조건 `none`** 이다 — 자동으로 화면을 바꾸면 사람이 치던 것을 밀어낸다.
98
+ * 모드보다 이 규칙이 앞선다.
99
+ */
100
+ export declare function planFollow(mode: FollowMode, opened: boolean, editing: boolean): 'none' | 'show' | 'open' | 'stage';