@elyun/bylane 1.19.0 → 1.21.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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@elyun/bylane",
3
- "version": "1.19.0",
3
+ "version": "1.21.0",
4
4
  "description": "Frontend development harness for Claude Code",
5
5
  "type": "module",
6
6
  "bin": {
@@ -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
- const runningLoops = Object.entries(lastStates)
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
- if (runningLoops.length === 0) {
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
- // blessed list 오버레이
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: 40,
62
- height: runningLoops.length + 4,
63
- label: ' 종료할 루프 선택 (Enter/Esc) ',
76
+ width: 54,
77
+ height: items.length + 6,
78
+ label: ' 종료할 항목 선택 ',
64
79
  tags: true,
65
80
  border: { type: 'line' },
66
81
  style: {
@@ -69,7 +84,13 @@ screen.key('s', () => {
69
84
  item: { fg: 'white' }
70
85
  },
71
86
  keys: true,
72
- items: runningLoops.map(l => ` ${l.name} (PID: ${l.pid})`)
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)
@@ -77,26 +98,42 @@ screen.key('s', () => {
77
98
  screen.render()
78
99
 
79
100
  list.on('select', (item, idx) => {
80
- const loop = runningLoops[idx]
81
- try {
82
- process.kill(loop.pid, 'SIGTERM')
83
- header.update({
84
- workflowTitle: `${loop.name} 종료 요청됨 (PID: ${loop.pid})`,
85
- time: new Date().toLocaleTimeString('ko-KR', { hour12: false }),
86
- elapsed: `${Math.floor((Date.now() - startTime) / 1000)}s`
87
- })
88
- } catch {
89
- header.update({
90
- workflowTitle: `${loop.name} 종료 실패 (이미 종료됨?)`,
91
- time: new Date().toLocaleTimeString('ko-KR', { hour12: false }),
92
- elapsed: `${Math.floor((Date.now() - startTime) / 1000)}s`
93
- })
101
+ const target = items[idx]
102
+ if (!target) { screen.remove(list); screen.render(); return }
103
+ let msg
104
+
105
+ if (target.type === 'loop') {
106
+ // 루프: SIGTERM
107
+ try {
108
+ process.kill(Number(target.pid), 'SIGTERM')
109
+ msg = `${target.name} 종료 요청됨 (PID: ${target.pid})`
110
+ } catch {
111
+ msg = `${target.name} 종료 실패 (이미 종료됨?)`
112
+ }
113
+ } else {
114
+ // 에이전트: 상태를 cancelled로 기록 + cancel.json 생성
115
+ try {
116
+ const cur = lastStates[target.name] ?? {}
117
+ writeState(target.name, { ...cur, status: 'cancelled', cancelledAt: new Date().toISOString() }, STATE_DIR)
118
+ } catch {}
119
+ // cancel.json 생성 (훅에서 새 Agent 호출 차단)
120
+ if (!existsSync(CANCEL_FILE)) {
121
+ mkdirSync(STATE_DIR, { recursive: true })
122
+ writeFileSync(CANCEL_FILE, JSON.stringify({ cancelledAt: new Date().toISOString() }))
123
+ }
124
+ msg = `${target.name} 취소 요청됨 (Claude 세션에서 완전 종료 필요)`
94
125
  }
126
+
127
+ header.update({
128
+ workflowTitle: msg,
129
+ time: new Date().toLocaleTimeString('ko-KR', { hour12: false }),
130
+ elapsed: `${Math.floor((Date.now() - startTime) / 1000)}s`
131
+ })
95
132
  screen.remove(list)
96
133
  screen.render()
97
134
  })
98
135
 
99
- list.key(['escape', 'q'], () => {
136
+ list.key(['escape'], () => {
100
137
  screen.remove(list)
101
138
  screen.render()
102
139
  })
@@ -62,7 +62,7 @@ export function createLayout() {
62
62
  left: 0,
63
63
  width: '100%',
64
64
  height: 1,
65
- content: ' [q]종료 [r]상태정리 [c]에이전트취소토글 [s]루프종료 [Tab]포커스 [j/k]로그스크롤',
65
+ content: ' [q]종료 [r]상태정리 [c]전체취소토글 [s]선택종료(루프/에이전트) [Tab]포커스 [j/k]로그스크롤',
66
66
  style: { fg: 'black', bg: 'cyan' }
67
67
  })
68
68
  screen.append(footer)
@@ -18,6 +18,21 @@ const STATUS_ICON = {
18
18
  escalated: '[!]'
19
19
  }
20
20
 
21
+ const STATUS_COLOR = {
22
+ idle: '',
23
+ in_progress: '{yellow-fg}',
24
+ running: '{green-fg}',
25
+ completed: '{green-fg}',
26
+ stopped: '{grey-fg}',
27
+ failed: '{red-fg}',
28
+ escalated: '{red-fg}'
29
+ }
30
+
31
+ function colorize(text, status) {
32
+ const c = STATUS_COLOR[status] ?? ''
33
+ return c ? `${c}${text}{/}` : text
34
+ }
35
+
21
36
  export function createPipelinePanel(screen) {
22
37
  const box = blessed.box({
23
38
  top: 3,
@@ -35,7 +50,7 @@ export function createPipelinePanel(screen) {
35
50
  update(states, subagents = { active: [], recent: [] }) {
36
51
  const lines = AGENTS.map(name => {
37
52
  const s = states[name]
38
- if (!s) return ` ${STATUS_ICON.idle} ${name.padEnd(16)} 대기`
53
+ if (!s) return ` {grey-fg}${STATUS_ICON.idle} ${name.padEnd(16)} 대기{/}`
39
54
  const icon = STATUS_ICON[s.status] ?? STATUS_ICON.idle
40
55
  const elapsed = s.startedAt
41
56
  ? `${Math.floor((Date.now() - new Date(s.startedAt)) / 1000)}s`
@@ -43,7 +58,7 @@ export function createPipelinePanel(screen) {
43
58
  const bar = s.progress > 0
44
59
  ? `${'#'.repeat(Math.floor(s.progress / 10))}${'-'.repeat(10 - Math.floor(s.progress / 10))} ${s.progress}%`
45
60
  : ''
46
- return ` ${icon} ${name.padEnd(16)} ${elapsed.padEnd(6)} ${bar}`
61
+ return ` ${colorize(`${icon} ${name.padEnd(16)}`, s.status)} ${elapsed.padEnd(6)} ${bar}`
47
62
  })
48
63
 
49
64
  const retries = states['orchestrator']?.retries ?? 0
@@ -57,13 +72,13 @@ export function createPipelinePanel(screen) {
57
72
  for (const name of allLoops) {
58
73
  const s = states[name]
59
74
  if (!s) {
60
- lines.push(` [-] ${name.padEnd(16)} 미실행`)
75
+ lines.push(` {grey-fg}[-] ${name.padEnd(16)} 미실행{/}`)
61
76
  } else {
62
77
  const icon = STATUS_ICON[s.status] ?? '[-]'
63
78
  const elapsed = s.startedAt
64
79
  ? `${Math.floor((Date.now() - new Date(s.startedAt)) / 1000)}s`
65
80
  : ''
66
- lines.push(` ${icon} ${name.padEnd(16)} ${elapsed}`)
81
+ lines.push(` ${colorize(`${icon} ${name.padEnd(16)}`, s.status)} ${elapsed}`)
67
82
  }
68
83
  }
69
84