@dmsdc-ai/aigentry-deliberation 0.0.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.
@@ -0,0 +1,21 @@
1
+ {
2
+ "provider": "chatgpt-extension",
3
+ "version": "1.0.0",
4
+ "domains": [],
5
+ "titlePatterns": ["ChatGPT"],
6
+ "isExtension": true,
7
+ "selectors": {
8
+ "inputSelector": "#prompt-textarea",
9
+ "sendButton": "button[data-testid='send-button']",
10
+ "responseSelector": ".markdown.prose",
11
+ "responseContainer": "[data-message-author-role='assistant']",
12
+ "streamingIndicator": ".result-streaming"
13
+ },
14
+ "timing": {
15
+ "inputDelayMs": 100,
16
+ "sendDelayMs": 300,
17
+ "pollIntervalMs": 500,
18
+ "streamingTimeoutMs": 45000
19
+ },
20
+ "notes": "ChatGPT extension side panel - placeholder selectors, update after DOM inspection"
21
+ }
@@ -0,0 +1,20 @@
1
+ {
2
+ "provider": "chatgpt",
3
+ "version": "1.0.0",
4
+ "domains": ["chat.openai.com", "chatgpt.com"],
5
+ "selectors": {
6
+ "inputSelector": "#prompt-textarea",
7
+ "sendButton": "button[data-testid='send-button']",
8
+ "responseSelector": ".markdown.prose",
9
+ "responseContainer": "[data-message-author-role='assistant']",
10
+ "streamingIndicator": ".result-streaming",
11
+ "conversationList": "nav[aria-label='Chat history']"
12
+ },
13
+ "timing": {
14
+ "inputDelayMs": 100,
15
+ "sendDelayMs": 200,
16
+ "pollIntervalMs": 500,
17
+ "streamingTimeoutMs": 45000
18
+ },
19
+ "notes": "ChatGPT DOM selectors - update version when selectors change"
20
+ }
@@ -0,0 +1,21 @@
1
+ {
2
+ "provider": "claude-extension",
3
+ "version": "1.0.0",
4
+ "domains": [],
5
+ "titlePatterns": ["Claude"],
6
+ "isExtension": true,
7
+ "selectors": {
8
+ "inputSelector": "[contenteditable='true']",
9
+ "sendButton": "button[aria-label='Send']",
10
+ "responseSelector": ".prose",
11
+ "responseContainer": "[data-is-streaming]",
12
+ "streamingIndicator": "[data-is-streaming='true']"
13
+ },
14
+ "timing": {
15
+ "inputDelayMs": 100,
16
+ "sendDelayMs": 300,
17
+ "pollIntervalMs": 500,
18
+ "streamingTimeoutMs": 45000
19
+ },
20
+ "notes": "Claude extension side panel - placeholder selectors, update after DOM inspection"
21
+ }
@@ -0,0 +1,19 @@
1
+ {
2
+ "provider": "claude",
3
+ "version": "1.0.0",
4
+ "domains": ["claude.ai"],
5
+ "selectors": {
6
+ "inputSelector": "div.ProseMirror[contenteditable='true']",
7
+ "sendButton": "button[aria-label='Send Message'], button[data-testid='send-button']",
8
+ "responseSelector": ".font-claude-message .grid-cols-1 .grid .min-w-0",
9
+ "responseContainer": "[data-is-streaming]",
10
+ "streamingIndicator": "[data-is-streaming='true']"
11
+ },
12
+ "timing": {
13
+ "inputDelayMs": 100,
14
+ "sendDelayMs": 300,
15
+ "pollIntervalMs": 500,
16
+ "streamingTimeoutMs": 60000
17
+ },
18
+ "notes": "Claude web selectors - verify via DOM inspection if selectors change"
19
+ }
@@ -0,0 +1,24 @@
1
+ {
2
+ "version": "1.0.0",
3
+ "description": "Maps Chrome extension side panel titles to LLM providers for deliberation tab detection",
4
+ "providers": [
5
+ {
6
+ "provider": "claude-extension",
7
+ "selectorConfig": "claude-extension",
8
+ "titlePatterns": ["Claude"],
9
+ "notes": "Anthropic Claude Chrome extension side panel"
10
+ },
11
+ {
12
+ "provider": "chatgpt-extension",
13
+ "selectorConfig": "chatgpt-extension",
14
+ "titlePatterns": ["ChatGPT"],
15
+ "notes": "OpenAI ChatGPT Chrome extension side panel"
16
+ },
17
+ {
18
+ "provider": "gemini-extension",
19
+ "selectorConfig": "gemini-extension",
20
+ "titlePatterns": ["Gemini"],
21
+ "notes": "Google Gemini Chrome extension side panel"
22
+ }
23
+ ]
24
+ }
@@ -0,0 +1,21 @@
1
+ {
2
+ "provider": "gemini-extension",
3
+ "version": "1.0.0",
4
+ "domains": [],
5
+ "titlePatterns": ["Gemini"],
6
+ "isExtension": true,
7
+ "selectors": {
8
+ "inputSelector": ".ql-editor",
9
+ "sendButton": "button[aria-label='Send message']",
10
+ "responseSelector": ".response-content",
11
+ "responseContainer": ".model-response-text",
12
+ "streamingIndicator": ".loading-indicator"
13
+ },
14
+ "timing": {
15
+ "inputDelayMs": 100,
16
+ "sendDelayMs": 300,
17
+ "pollIntervalMs": 500,
18
+ "streamingTimeoutMs": 45000
19
+ },
20
+ "notes": "Gemini extension side panel - placeholder selectors, update after DOM inspection"
21
+ }
@@ -0,0 +1,19 @@
1
+ {
2
+ "provider": "gemini",
3
+ "version": "1.0.0",
4
+ "domains": ["gemini.google.com"],
5
+ "selectors": {
6
+ "inputSelector": ".ql-editor[contenteditable='true']",
7
+ "sendButton": "button.send-button, button[aria-label='Send message']",
8
+ "responseSelector": ".markdown.markdown-main-panel",
9
+ "responseContainer": ".model-response-text",
10
+ "streamingIndicator": ".loading-indicator, .model-response-text .loading"
11
+ },
12
+ "timing": {
13
+ "inputDelayMs": 100,
14
+ "sendDelayMs": 300,
15
+ "pollIntervalMs": 500,
16
+ "streamingTimeoutMs": 45000
17
+ },
18
+ "notes": "Gemini web selectors - verify via DOM inspection if selectors change"
19
+ }
@@ -0,0 +1,28 @@
1
+ {
2
+ "presets": {
3
+ "balanced": {
4
+ "description": "균형 잡힌 토론 — 비판, 구현, 중재 관점",
5
+ "roles": ["critic", "implementer", "mediator"]
6
+ },
7
+ "debate": {
8
+ "description": "찬반 토론 — 비판자 중심 + 중재자",
9
+ "roles": ["critic", "critic", "mediator"]
10
+ },
11
+ "research": {
12
+ "description": "연구 중심 — 연구자 다수 + 비판적 검증",
13
+ "roles": ["researcher", "researcher", "critic"]
14
+ },
15
+ "brainstorm": {
16
+ "description": "자유 브레인스토밍 — 역할 제한 없음",
17
+ "roles": ["free", "free", "free"]
18
+ },
19
+ "review": {
20
+ "description": "코드/설계 리뷰 — 비판, 구현, 연구 관점",
21
+ "roles": ["critic", "implementer", "researcher"]
22
+ },
23
+ "consensus": {
24
+ "description": "합의 도출 — 중재자 중심",
25
+ "roles": ["mediator", "critic", "implementer"]
26
+ }
27
+ }
28
+ }
@@ -0,0 +1,12 @@
1
+ 당신은 **비판자(Critic)**입니다. 모든 제안의 약점, 리스크, 논리적 결함을 찾으세요.
2
+ 반대를 위한 반대가 아니라, 건설적 비판으로 토론의 질을 높이세요.
3
+
4
+ 응답 구조:
5
+ ## 비판
6
+ (구체적 문제점을 나열)
7
+
8
+ ## 근거
9
+ (왜 문제인지 증거/논리 기반으로 설명)
10
+
11
+ ## 심각도
12
+ minor / major / blocking 중 택1
@@ -0,0 +1 @@
1
+ 자유롭게 의견을 제시하세요. 특정 역할에 구애받지 않고 토론 주제에 대해 자신의 관점을 공유합니다.
@@ -0,0 +1,12 @@
1
+ 당신은 **구현자(Implementer)**입니다. 제안의 실현 가능성을 분석하고 구체적 구현 방안을 제시하세요.
2
+ 추상적 아이디어를 실행 가능한 계획으로 변환하세요.
3
+
4
+ 응답 구조:
5
+ ## 제안
6
+ (구체적 구현 방안)
7
+
8
+ ## 구현 난이도
9
+ low / medium / high 중 택1 + 예상 코드 변경량
10
+
11
+ ## 코드 스케치
12
+ (핵심 로직의 의사 코드 또는 실제 코드)
@@ -0,0 +1,12 @@
1
+ 당신은 **중재자(Mediator)**입니다. 참가자들의 의견을 종합하고 합의점을 찾으세요.
2
+ 갈등을 해소하고 생산적인 결론을 도출하세요.
3
+
4
+ 응답 구조:
5
+ ## 합의점
6
+ (참가자들이 동의하는 사항 정리)
7
+
8
+ ## 미해결
9
+ (아직 합의되지 않은 논점)
10
+
11
+ ## 권고
12
+ (중재자로서의 최종 권고안)
@@ -0,0 +1,12 @@
1
+ 당신은 **연구자(Researcher)**입니다. 외부 사례, 데이터, 벤치마크로 토론을 뒷받침하세요.
2
+ 주장에는 반드시 근거를 제시하세요.
3
+
4
+ 응답 구조:
5
+ ## 조사 결과
6
+ (관련 사례, 데이터, 기존 솔루션)
7
+
8
+ ## 비교 분석
9
+ (대안들의 장단점 비교표)
10
+
11
+ ## 근거 기반 권고
12
+ (데이터에 기반한 추천)
@@ -0,0 +1,94 @@
1
+ #!/usr/bin/env node
2
+ "use strict";
3
+
4
+ // Usage: node session-monitor-win.js <sessionId> <project>
5
+ const fs = require("fs");
6
+ const path = require("path");
7
+
8
+ const sessionId = process.argv[2];
9
+ const project = process.argv[3] || "default";
10
+
11
+ if (!sessionId) {
12
+ console.error("Usage: node session-monitor-win.js <sessionId> <project>");
13
+ process.exit(1);
14
+ }
15
+
16
+ const HOME = process.env.HOME || process.env.USERPROFILE || "";
17
+ const stateDir = path.join(HOME, ".local", "state", "mcp-deliberation", project);
18
+ const stateFile = path.join(stateDir, `${sessionId}.json`);
19
+
20
+ const BOLD = "\x1b[1m";
21
+ const CYAN = "\x1b[36m";
22
+ const GREEN = "\x1b[32m";
23
+ const YELLOW = "\x1b[33m";
24
+ const DIM = "\x1b[2m";
25
+ const NC = "\x1b[0m";
26
+
27
+ let prevData = "";
28
+
29
+ function render() {
30
+ let raw;
31
+ try {
32
+ raw = fs.readFileSync(stateFile, "utf-8");
33
+ } catch {
34
+ return; // file not ready yet
35
+ }
36
+ if (raw === prevData) return;
37
+ prevData = raw;
38
+
39
+ let state;
40
+ try {
41
+ state = JSON.parse(raw);
42
+ } catch {
43
+ return;
44
+ }
45
+
46
+ console.clear();
47
+ console.log(`${BOLD}${CYAN}╔═══════════════════════════════════════╗${NC}`);
48
+ console.log(`${BOLD}${CYAN}║ Deliberation Monitor ║${NC}`);
49
+ console.log(`${BOLD}${CYAN}╚═══════════════════════════════════════╝${NC}`);
50
+ console.log();
51
+ console.log(`${BOLD}Topic:${NC} ${state.topic || "(none)"}`);
52
+ console.log(`${BOLD}Round:${NC} ${state.current_round || "?"}/${state.max_rounds || "?"}`);
53
+ console.log(`${BOLD}Speaker:${NC} ${YELLOW}${state.current_speaker || "(waiting)"}${NC}`);
54
+ console.log(`${BOLD}Speakers:${NC} ${(state.speakers || []).join(", ")}`);
55
+ console.log();
56
+ console.log(`${DIM}${"─".repeat(50)}${NC}`);
57
+ console.log();
58
+
59
+ const log = Array.isArray(state.log) ? state.log : [];
60
+ const recent = log.slice(-10);
61
+ for (const entry of recent) {
62
+ const speaker = entry.speaker || "unknown";
63
+ const content = String(entry.content || "").slice(0, 300);
64
+ const round = entry.round != null ? ` (R${entry.round})` : "";
65
+ console.log(`${GREEN}[${speaker}]${NC}${DIM}${round}${NC}`);
66
+ console.log(content);
67
+ console.log();
68
+ }
69
+
70
+ if (recent.length === 0) {
71
+ console.log(`${DIM}(No messages yet. Waiting for first turn...)${NC}`);
72
+ }
73
+
74
+ console.log(`${DIM}${"─".repeat(50)}${NC}`);
75
+ console.log(`${DIM}Auto-refresh every 2s | Session: ${sessionId} | Ctrl+C to close${NC}`);
76
+ }
77
+
78
+ // Initial render
79
+ render();
80
+
81
+ // Poll every 2 seconds
82
+ const interval = setInterval(render, 2000);
83
+
84
+ // Graceful shutdown
85
+ process.on("SIGINT", () => {
86
+ clearInterval(interval);
87
+ console.log(`\n${DIM}Monitor closed.${NC}`);
88
+ process.exit(0);
89
+ });
90
+
91
+ process.on("SIGTERM", () => {
92
+ clearInterval(interval);
93
+ process.exit(0);
94
+ });
@@ -0,0 +1,316 @@
1
+ #!/bin/bash
2
+ #
3
+ # Session Monitor — 단일 deliberation 세션 전용 터미널 뷰
4
+ #
5
+ # Usage:
6
+ # bash session-monitor.sh <session_id> <project_slug>
7
+ #
8
+ # MCP 서버가 deliberation_start 시 자동으로 tmux 윈도우에서 실행합니다.
9
+ #
10
+
11
+ SESSION_ID="${1:?session_id 필요}"
12
+ PROJECT="${2:?project_slug 필요}"
13
+ STATE_FILE="$HOME/.local/lib/mcp-deliberation/state/$PROJECT/sessions/$SESSION_ID.json"
14
+
15
+ # 색상
16
+ RED='\033[0;31m'
17
+ GREEN='\033[0;32m'
18
+ YELLOW='\033[1;33m'
19
+ BLUE='\033[0;34m'
20
+ CYAN='\033[0;36m'
21
+ MAGENTA='\033[0;35m'
22
+ BOLD='\033[1m'
23
+ DIM='\033[2m'
24
+ NC='\033[0m'
25
+ BOX_CONTENT_WIDTH=60
26
+ BOX_RULE="$(printf '%*s' "$((BOX_CONTENT_WIDTH + 2))" '' | tr ' ' '═')"
27
+
28
+ fit_to_width() {
29
+ TEXT="$1" WIDTH="$2" node -e "
30
+ const raw = String(process.env.TEXT ?? '')
31
+ .replace(/\s+/g, ' ')
32
+ .trim();
33
+ const target = Number(process.env.WIDTH ?? '60');
34
+
35
+ const isWide = (cp) =>
36
+ cp >= 0x1100 && (
37
+ cp <= 0x115f || cp === 0x2329 || cp === 0x232a ||
38
+ (cp >= 0x2e80 && cp <= 0xa4cf && cp !== 0x303f) ||
39
+ (cp >= 0xac00 && cp <= 0xd7a3) ||
40
+ (cp >= 0xf900 && cp <= 0xfaff) ||
41
+ (cp >= 0xfe10 && cp <= 0xfe19) ||
42
+ (cp >= 0xfe30 && cp <= 0xfe6f) ||
43
+ (cp >= 0xff00 && cp <= 0xff60) ||
44
+ (cp >= 0xffe0 && cp <= 0xffe6) ||
45
+ (cp >= 0x1f300 && cp <= 0x1f64f) ||
46
+ (cp >= 0x1f900 && cp <= 0x1f9ff) ||
47
+ (cp >= 0x20000 && cp <= 0x3fffd)
48
+ );
49
+
50
+ const charWidth = (ch) => {
51
+ const cp = ch.codePointAt(0);
52
+ if (!cp || cp < 32 || (cp >= 0x7f && cp < 0xa0)) return 0;
53
+ return isWide(cp) ? 2 : 1;
54
+ };
55
+
56
+ const src = Array.from(raw);
57
+ const out = [];
58
+ let width = 0;
59
+ let truncated = false;
60
+ for (const ch of src) {
61
+ const w = charWidth(ch);
62
+ if (width + w > target) {
63
+ truncated = true;
64
+ break;
65
+ }
66
+ out.push(ch);
67
+ width += w;
68
+ }
69
+
70
+ if (truncated && target >= 3) {
71
+ while (out.length > 0 && width + 3 > target) {
72
+ const last = out.pop();
73
+ width -= charWidth(last);
74
+ }
75
+ out.push('.', '.', '.');
76
+ width += 3;
77
+ }
78
+
79
+ process.stdout.write(out.join('') + ' '.repeat(Math.max(0, target - width)));
80
+ " 2>/dev/null
81
+ }
82
+
83
+ print_box_border() {
84
+ printf "%b%s%b\n" "$BOLD" "$1${BOX_RULE}$2" "$NC"
85
+ }
86
+
87
+ print_box_row() {
88
+ local text="$1"
89
+ local color="${2:-$NC}"
90
+ local fitted
91
+ fitted="$(fit_to_width "$text" "$BOX_CONTENT_WIDTH")"
92
+ printf "%b║%b %b%s%b %b║%b\n" "$BOLD" "$NC" "$color" "$fitted" "$NC" "$BOLD" "$NC"
93
+ }
94
+
95
+ pane_in_copy_mode() {
96
+ if [ -z "$TMUX" ] || [ -z "${TMUX_PANE:-}" ]; then
97
+ return 1
98
+ fi
99
+ [ "$(tmux display-message -p -t "$TMUX_PANE" "#{pane_in_mode}" 2>/dev/null)" = "1" ]
100
+ }
101
+
102
+ state_signature() {
103
+ if [ ! -f "$STATE_FILE" ]; then
104
+ echo "MISSING"
105
+ return
106
+ fi
107
+
108
+ node -e "
109
+ const fs = require('fs');
110
+ try {
111
+ const s = JSON.parse(fs.readFileSync('$STATE_FILE','utf-8'));
112
+ const logs = Array.isArray(s.log) ? s.log : [];
113
+ const last = logs.length > 0 ? logs[logs.length - 1] : {};
114
+ const sig = [
115
+ s.status ?? '',
116
+ s.current_round ?? '',
117
+ s.max_rounds ?? '',
118
+ s.current_speaker ?? '',
119
+ Array.isArray(s.speakers) ? s.speakers.join(',') : '',
120
+ logs.length,
121
+ last.timestamp ?? '',
122
+ s.updated ?? '',
123
+ s.synthesis ? s.synthesis.length : 0
124
+ ].join('|');
125
+ console.log(sig);
126
+ } catch {
127
+ console.log('PARSE_ERROR');
128
+ }
129
+ " 2>/dev/null
130
+ }
131
+
132
+ get_field() {
133
+ node -e "
134
+ try {
135
+ const d = JSON.parse(require('fs').readFileSync('$STATE_FILE','utf-8'));
136
+ const keys = '$1'.split('.');
137
+ let val = d;
138
+ for (const k of keys) val = val?.[k];
139
+ if (Array.isArray(val)) console.log(val.length);
140
+ else console.log(val ?? '?');
141
+ } catch { console.log('?'); }
142
+ " 2>/dev/null
143
+ }
144
+
145
+ render() {
146
+ if [ ! -f "$STATE_FILE" ]; then
147
+ echo -e "${DIM}세션 파일 대기 중: $STATE_FILE${NC}"
148
+ return
149
+ fi
150
+
151
+ local topic=$(get_field "topic")
152
+ local status=$(get_field "status")
153
+ local round=$(get_field "current_round")
154
+ local max_rounds=$(get_field "max_rounds")
155
+ local participant_count=$(get_field "speakers")
156
+ local speaker=$(get_field "current_speaker")
157
+ local responses=$(get_field "log")
158
+
159
+ # 상태 색상
160
+ local status_color="$YELLOW"
161
+ case "$status" in
162
+ active) status_color="$GREEN" ;;
163
+ completed) status_color="$CYAN" ;;
164
+ awaiting_synthesis) status_color="$BLUE" ;;
165
+ esac
166
+
167
+ # 프로그레스 바
168
+ if ! [[ "$participant_count" =~ ^[0-9]+$ ]]; then participant_count=2; fi
169
+ if [ "$participant_count" -lt 1 ]; then participant_count=1; fi
170
+
171
+ local total=$((max_rounds * participant_count))
172
+ local filled=$responses
173
+ [ "$filled" -gt "$total" ] 2>/dev/null && filled=$total
174
+ local bar=""
175
+ for ((i=0; i<filled; i++)); do bar+="█"; done
176
+ for ((i=filled; i<total; i++)); do bar+="░"; done
177
+
178
+ # 헤더
179
+ print_box_border "╔" "╗"
180
+ print_box_row "$topic" "$YELLOW"
181
+ print_box_border "╠" "╣"
182
+ print_box_row "Session: $SESSION_ID" "$MAGENTA"
183
+ print_box_row "Project: $PROJECT" "$CYAN"
184
+ print_box_row "Status: $status" "$status_color"
185
+ print_box_row "Round: $round/$max_rounds | Next: $speaker" "$BOLD"
186
+ print_box_row "Progress: [$bar] $responses/$total" "$GREEN"
187
+ print_box_border "╚" "╝"
188
+ echo ""
189
+
190
+ # 토론 기록
191
+ node -e "
192
+ const fs = require('fs');
193
+ try {
194
+ const s = JSON.parse(fs.readFileSync('$STATE_FILE','utf-8'));
195
+
196
+ if (s.synthesis) {
197
+ console.log('\x1b[1m── Synthesis ──\x1b[0m');
198
+ console.log('');
199
+ const lines = s.synthesis.split('\n').slice(0, 20);
200
+ lines.forEach(l => console.log(' ' + l));
201
+ if (s.synthesis.split('\n').length > 20) console.log(' ...(truncated)');
202
+ console.log('');
203
+ }
204
+
205
+ if (s.log.length === 0) {
206
+ console.log('\x1b[2m 아직 응답이 없습니다. ' + s.current_speaker + ' 차례 대기 중...\x1b[0m');
207
+ process.exit(0);
208
+ }
209
+
210
+ console.log('\x1b[1m── Debate Log ──\x1b[0m');
211
+ console.log('');
212
+
213
+ const palette = ['\x1b[34m', '\x1b[33m', '\x1b[35m', '\x1b[36m', '\x1b[32m', '\x1b[31m'];
214
+ const icons = ['🔵', '🟡', '🟣', '🟢', '🟠', '⚪'];
215
+ const hash = (name) => {
216
+ let out = 0;
217
+ for (let i = 0; i < name.length; i += 1) out = (out * 31 + name.charCodeAt(i)) >>> 0;
218
+ return out;
219
+ };
220
+ const styleFor = (name) => {
221
+ const idx = hash(String(name ?? '')) % palette.length;
222
+ return { color: palette[idx], icon: icons[idx % icons.length] };
223
+ };
224
+
225
+ for (const entry of s.log) {
226
+ const { color, icon } = styleFor(entry.speaker);
227
+ console.log(color + '\x1b[1m' + icon + ' ' + entry.speaker + ' — Round ' + entry.round + '\x1b[0m');
228
+
229
+ const lines = entry.content.split('\n');
230
+ lines.forEach(l => console.log(' ' + l));
231
+ console.log('');
232
+ }
233
+
234
+ if (s.status === 'active') {
235
+ const nextColor = styleFor(s.current_speaker).color;
236
+ console.log(nextColor + ' ⏳ Waiting for ' + s.current_speaker + ' (Round ' + s.current_round + ')...\x1b[0m');
237
+ } else if (s.status === 'awaiting_synthesis') {
238
+ console.log('\x1b[36m 🏁 모든 라운드 종료. 합성 대기 중...\x1b[0m');
239
+ }
240
+ } catch(e) {
241
+ console.log(' 읽기 실패: ' + e.message);
242
+ }
243
+ " 2>/dev/null
244
+
245
+ echo ""
246
+
247
+ # 완료 시 카운트다운
248
+ if [ "$status" = "completed" ]; then
249
+ echo -e "${CYAN}${BOLD} ✅ Deliberation 완료!${NC}"
250
+ echo -e "${DIM} 이 터미널은 30초 후 자동으로 닫힙니다...${NC}"
251
+ for i in $(seq 30 -1 1); do
252
+ printf "\r${DIM} 닫히기까지 %2d초...${NC}" "$i"
253
+ sleep 1
254
+ # 파일이 삭제되었으면 즉시 종료
255
+ [ ! -f "$STATE_FILE" ] && break
256
+ done
257
+ echo ""
258
+ exit 0
259
+ fi
260
+ }
261
+
262
+ draw_frame() {
263
+ # 전체 클리어 대신 홈 이동 + 하단 잔여 영역만 정리해서 깜빡임 감소
264
+ printf '\033[H'
265
+ render
266
+ printf '\033[J'
267
+ echo -e "${DIM}Auto-refresh on change (poll 2s) | Scroll: mouse wheel / PgUp (tmux copy-mode) | Ctrl+C${NC}"
268
+ }
269
+
270
+ # 커서 숨김 (종료 시 복구)
271
+ printf '\033[?25l'
272
+ trap 'printf "\033[?25h"' EXIT INT TERM
273
+ printf '\033[2J\033[H'
274
+
275
+ # tmux에서 스크롤(휠/업다운) 동작을 위해 mouse/copy history 옵션 활성화
276
+ if [ -n "$TMUX" ]; then
277
+ tmux set-option -g mouse on >/dev/null 2>&1 || true
278
+ tmux set-option -g history-limit 200000 >/dev/null 2>&1 || true
279
+ fi
280
+
281
+ # 메인 루프
282
+ last_sig=""
283
+ seen_file=0
284
+ while true; do
285
+ if pane_in_copy_mode; then
286
+ # 사용자가 스크롤 중이면 렌더 업데이트를 멈춰 화면 점프를 방지
287
+ sleep 1
288
+ continue
289
+ fi
290
+
291
+ if [ ! -f "$STATE_FILE" ]; then
292
+ if [ "$seen_file" -eq 1 ]; then
293
+ printf '\033[H\033[J'
294
+ echo -e "${RED}세션이 삭제되었습니다.${NC}"
295
+ sleep 3
296
+ exit 0
297
+ fi
298
+
299
+ if [ "$last_sig" != "MISSING" ]; then
300
+ draw_frame
301
+ last_sig="MISSING"
302
+ fi
303
+
304
+ sleep 2
305
+ continue
306
+ fi
307
+
308
+ seen_file=1
309
+ sig="$(state_signature)"
310
+ if [ "$sig" != "$last_sig" ]; then
311
+ draw_frame
312
+ last_sig="$sig"
313
+ fi
314
+
315
+ sleep 2
316
+ done