@elyun/bylane 1.20.0 → 1.22.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/package.json +1 -1
- package/src/monitor/index.js +70 -25
- package/src/monitor/layout.js +1 -1
package/package.json
CHANGED
package/src/monitor/index.js
CHANGED
|
@@ -5,6 +5,7 @@ import { createPoller } from './poller.js'
|
|
|
5
5
|
import { existsSync, readFileSync, writeFileSync, unlinkSync, mkdirSync } from 'fs'
|
|
6
6
|
import { join } from 'path'
|
|
7
7
|
import { runCleanup, formatCleanupResult } from '../cleanup.js'
|
|
8
|
+
import { writeState } from '../state.js'
|
|
8
9
|
|
|
9
10
|
const { screen, header, pipeline, log, queue, status, onCleanup } = createLayout()
|
|
10
11
|
const poller = createPoller()
|
|
@@ -38,15 +39,24 @@ poller.onChange((states) => {
|
|
|
38
39
|
status.update()
|
|
39
40
|
})
|
|
40
41
|
|
|
41
|
-
// 's' — 실행 중인
|
|
42
|
+
// 's' — 실행 중인 루프/에이전트 선택 종료
|
|
42
43
|
screen.key('s', () => {
|
|
43
|
-
|
|
44
|
+
// 루프: running + pid 있는 것
|
|
45
|
+
const loops = Object.entries(lastStates)
|
|
44
46
|
.filter(([name, s]) => name.endsWith('-loop') && s?.status === 'running' && s?.pid)
|
|
45
|
-
.map(([name, s]) => ({ name, pid: s.pid }))
|
|
47
|
+
.map(([name, s]) => ({ name, type: 'loop', pid: s.pid }))
|
|
46
48
|
|
|
47
|
-
|
|
49
|
+
// 에이전트: in_progress 상태인 것
|
|
50
|
+
const agents = Object.entries(lastStates)
|
|
51
|
+
.filter(([name, s]) => !name.endsWith('-loop') && !name.endsWith('-queue')
|
|
52
|
+
&& s?.status === 'in_progress')
|
|
53
|
+
.map(([name, s]) => ({ name, type: 'agent', task: s.currentTask ?? '' }))
|
|
54
|
+
|
|
55
|
+
const items = [...loops, ...agents]
|
|
56
|
+
|
|
57
|
+
if (items.length === 0) {
|
|
48
58
|
header.update({
|
|
49
|
-
workflowTitle: '실행
|
|
59
|
+
workflowTitle: '종료할 실행 중 항목 없음',
|
|
50
60
|
time: new Date().toLocaleTimeString('ko-KR', { hour12: false }),
|
|
51
61
|
elapsed: `${Math.floor((Date.now() - startTime) / 1000)}s`
|
|
52
62
|
})
|
|
@@ -54,13 +64,18 @@ screen.key('s', () => {
|
|
|
54
64
|
return
|
|
55
65
|
}
|
|
56
66
|
|
|
57
|
-
|
|
67
|
+
const listItems = items.map(item =>
|
|
68
|
+
item.type === 'loop'
|
|
69
|
+
? ` [루프] ${item.name.padEnd(18)} PID: ${item.pid}`
|
|
70
|
+
: ` [에이전트] ${item.name.padEnd(15)} ${item.task.slice(0, 20)}`
|
|
71
|
+
)
|
|
72
|
+
|
|
58
73
|
const list = blessed.list({
|
|
59
74
|
top: 'center',
|
|
60
75
|
left: 'center',
|
|
61
|
-
width:
|
|
62
|
-
height:
|
|
63
|
-
label: ' 종료할
|
|
76
|
+
width: 54,
|
|
77
|
+
height: items.length + 6,
|
|
78
|
+
label: ' 종료할 항목 선택 ',
|
|
64
79
|
tags: true,
|
|
65
80
|
border: { type: 'line' },
|
|
66
81
|
style: {
|
|
@@ -69,34 +84,64 @@ screen.key('s', () => {
|
|
|
69
84
|
item: { fg: 'white' }
|
|
70
85
|
},
|
|
71
86
|
keys: true,
|
|
72
|
-
|
|
87
|
+
vi: true,
|
|
88
|
+
mouse: true,
|
|
89
|
+
items: [
|
|
90
|
+
...listItems,
|
|
91
|
+
'',
|
|
92
|
+
' {grey-fg}↑↓/jk 이동 Enter 종료 Esc 취소{/}'
|
|
93
|
+
]
|
|
73
94
|
})
|
|
74
95
|
|
|
75
96
|
screen.append(list)
|
|
76
97
|
list.focus()
|
|
77
98
|
screen.render()
|
|
78
99
|
|
|
100
|
+
// 명시적 키 바인딩 (screen-level 핸들러와 충돌 방지)
|
|
101
|
+
list.key(['up', 'k'], () => { list.up(1); screen.render() })
|
|
102
|
+
list.key(['down', 'j'], () => { list.down(1); screen.render() })
|
|
103
|
+
list.key('enter', () => {
|
|
104
|
+
const idx = list.selected
|
|
105
|
+
list.emit('select', list.items[idx], idx)
|
|
106
|
+
})
|
|
107
|
+
|
|
79
108
|
list.on('select', (item, idx) => {
|
|
80
|
-
const
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
109
|
+
const target = items[idx]
|
|
110
|
+
if (!target) { screen.remove(list); screen.render(); return }
|
|
111
|
+
let msg
|
|
112
|
+
|
|
113
|
+
if (target.type === 'loop') {
|
|
114
|
+
// 루프: SIGTERM
|
|
115
|
+
try {
|
|
116
|
+
process.kill(Number(target.pid), 'SIGTERM')
|
|
117
|
+
msg = `${target.name} 종료 요청됨 (PID: ${target.pid})`
|
|
118
|
+
} catch {
|
|
119
|
+
msg = `${target.name} 종료 실패 (이미 종료됨?)`
|
|
120
|
+
}
|
|
121
|
+
} else {
|
|
122
|
+
// 에이전트: 상태를 cancelled로 기록 + cancel.json 생성
|
|
123
|
+
try {
|
|
124
|
+
const cur = lastStates[target.name] ?? {}
|
|
125
|
+
writeState(target.name, { ...cur, status: 'cancelled', cancelledAt: new Date().toISOString() }, STATE_DIR)
|
|
126
|
+
} catch {}
|
|
127
|
+
// cancel.json 생성 (훅에서 새 Agent 호출 차단)
|
|
128
|
+
if (!existsSync(CANCEL_FILE)) {
|
|
129
|
+
mkdirSync(STATE_DIR, { recursive: true })
|
|
130
|
+
writeFileSync(CANCEL_FILE, JSON.stringify({ cancelledAt: new Date().toISOString() }))
|
|
131
|
+
}
|
|
132
|
+
msg = `${target.name} 취소 요청됨 (Claude 세션에서 완전 종료 필요)`
|
|
94
133
|
}
|
|
134
|
+
|
|
135
|
+
header.update({
|
|
136
|
+
workflowTitle: msg,
|
|
137
|
+
time: new Date().toLocaleTimeString('ko-KR', { hour12: false }),
|
|
138
|
+
elapsed: `${Math.floor((Date.now() - startTime) / 1000)}s`
|
|
139
|
+
})
|
|
95
140
|
screen.remove(list)
|
|
96
141
|
screen.render()
|
|
97
142
|
})
|
|
98
143
|
|
|
99
|
-
list.key(['escape'
|
|
144
|
+
list.key(['escape'], () => {
|
|
100
145
|
screen.remove(list)
|
|
101
146
|
screen.render()
|
|
102
147
|
})
|
package/src/monitor/layout.js
CHANGED
|
@@ -62,7 +62,7 @@ export function createLayout() {
|
|
|
62
62
|
left: 0,
|
|
63
63
|
width: '100%',
|
|
64
64
|
height: 1,
|
|
65
|
-
content: ' [q]종료 [r]상태정리 [c]
|
|
65
|
+
content: ' [q]종료 [r]상태정리 [c]전체취소토글 [s]선택종료(루프/에이전트) [Tab]포커스 [j/k]로그스크롤',
|
|
66
66
|
style: { fg: 'black', bg: 'cyan' }
|
|
67
67
|
})
|
|
68
68
|
screen.append(footer)
|