@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.
- package/LICENSE +21 -0
- package/README.md +94 -0
- package/browser-control-port.js +563 -0
- package/degradation-state-machine.js +206 -0
- package/index.js +3156 -0
- package/install.js +202 -0
- package/observer.js +483 -0
- package/package.json +65 -0
- package/public/index.html +1478 -0
- package/selectors/chatgpt-extension.json +21 -0
- package/selectors/chatgpt.json +20 -0
- package/selectors/claude-extension.json +21 -0
- package/selectors/claude.json +19 -0
- package/selectors/extension-providers.json +24 -0
- package/selectors/gemini-extension.json +21 -0
- package/selectors/gemini.json +19 -0
- package/selectors/role-presets.json +28 -0
- package/selectors/roles/critic.md +12 -0
- package/selectors/roles/free.md +1 -0
- package/selectors/roles/implementer.md +12 -0
- package/selectors/roles/mediator.md +12 -0
- package/selectors/roles/researcher.md +12 -0
- package/session-monitor-win.js +94 -0
- package/session-monitor.sh +316 -0
- package/skills/deliberation/SKILL.md +164 -0
- package/skills/deliberation-executor/SKILL.md +86 -0
|
@@ -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 @@
|
|
|
1
|
+
자유롭게 의견을 제시하세요. 특정 역할에 구애받지 않고 토론 주제에 대해 자신의 관점을 공유합니다.
|
|
@@ -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
|