@k1e1n04/mav 0.1.18 → 0.1.19

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,28 @@
1
+ export interface KeyInfo {
2
+ name?: string;
3
+ ctrl?: boolean;
4
+ meta?: boolean;
5
+ shift?: boolean;
6
+ sequence?: string;
7
+ }
8
+ type KeypressHandler = (str: string, key: KeyInfo) => void;
9
+ type DataHandler = (chunk: string | Buffer) => void;
10
+ type ResizeHandler = () => void;
11
+ export declare class TerminalUI {
12
+ readonly input: NodeJS.ReadStream;
13
+ readonly output: NodeJS.WriteStream;
14
+ private resizeEmitter;
15
+ constructor(input?: NodeJS.ReadStream, output?: NodeJS.WriteStream);
16
+ get cols(): number;
17
+ get rows(): number;
18
+ onKeypress(handler: KeypressHandler): () => void;
19
+ onData(handler: DataHandler): () => void;
20
+ onResize(handler: ResizeHandler): () => void;
21
+ write(data: string): void;
22
+ clearScreen(): void;
23
+ render(content: string): void;
24
+ enterAlternateScreen(): void;
25
+ exitAlternateScreen(): void;
26
+ destroy(): void;
27
+ }
28
+ export {};
@@ -0,0 +1,74 @@
1
+ import { EventEmitter } from 'node:events';
2
+ import { emitKeypressEvents } from 'node:readline';
3
+ export class TerminalUI {
4
+ input;
5
+ output;
6
+ resizeEmitter = new EventEmitter();
7
+ constructor(input = process.stdin, output = process.stdout) {
8
+ this.input = input;
9
+ this.output = output;
10
+ emitKeypressEvents(this.input);
11
+ if (this.input.isTTY && typeof this.input.setRawMode === 'function') {
12
+ this.input.setRawMode(true);
13
+ }
14
+ this.input.resume();
15
+ if ('on' in this.output) {
16
+ this.output.on('resize', () => {
17
+ this.resizeEmitter.emit('resize');
18
+ });
19
+ }
20
+ this.write('\x1b]0;mav\x07');
21
+ }
22
+ get cols() {
23
+ return ('columns' in this.output && typeof this.output.columns === 'number')
24
+ ? this.output.columns
25
+ : 80;
26
+ }
27
+ get rows() {
28
+ return ('rows' in this.output && typeof this.output.rows === 'number')
29
+ ? this.output.rows
30
+ : 24;
31
+ }
32
+ onKeypress(handler) {
33
+ const wrapped = (str, key) => handler(str, key);
34
+ this.input.on('keypress', wrapped);
35
+ return () => {
36
+ this.input.off('keypress', wrapped);
37
+ };
38
+ }
39
+ onData(handler) {
40
+ this.input.on('data', handler);
41
+ return () => {
42
+ this.input.off('data', handler);
43
+ };
44
+ }
45
+ onResize(handler) {
46
+ this.resizeEmitter.on('resize', handler);
47
+ return () => {
48
+ this.resizeEmitter.off('resize', handler);
49
+ };
50
+ }
51
+ write(data) {
52
+ this.output.write(data);
53
+ }
54
+ clearScreen() {
55
+ this.write('\x1b[H\x1b[2J');
56
+ }
57
+ render(content) {
58
+ this.clearScreen();
59
+ this.write(content);
60
+ }
61
+ enterAlternateScreen() {
62
+ this.write('\x1b[?1049h');
63
+ }
64
+ exitAlternateScreen() {
65
+ this.write('\x1b[?1049l');
66
+ }
67
+ destroy() {
68
+ this.exitAlternateScreen();
69
+ if (this.input.isTTY && typeof this.input.setRawMode === 'function') {
70
+ this.input.setRawMode(false);
71
+ }
72
+ }
73
+ }
74
+ //# sourceMappingURL=terminal.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"terminal.js","sourceRoot":"","sources":["../../../src/ui/terminal.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@k1e1n04/mav",
3
- "version": "0.1.18",
3
+ "version": "0.1.19",
4
4
  "description": "Multi-agent view — manage multiple AI CLI sessions in one terminal",
5
5
  "type": "module",
6
6
  "bin": {
@@ -35,9 +35,7 @@
35
35
  "dependencies": {
36
36
  "commander": "13.1.0",
37
37
  "js-yaml": "4.1.0",
38
- "neo-blessed": "0.2.0",
39
- "node-pty": "1.0.0",
40
- "term.js": "^0.0.7"
38
+ "node-pty": "1.0.0"
41
39
  },
42
40
  "pnpm": {
43
41
  "onlyBuiltDependencies": [
@@ -46,7 +44,6 @@
46
44
  ]
47
45
  },
48
46
  "devDependencies": {
49
- "@types/blessed": "^0.1.27",
50
47
  "@types/js-yaml": "4.0.9",
51
48
  "@types/node": "22.15.21",
52
49
  "tsx": "4.19.3",
package/src/ui/app.ts CHANGED
@@ -1,95 +1,87 @@
1
- import blessed from 'neo-blessed'
2
- import type { Widgets } from 'neo-blessed'
3
1
  import type { AgentSession } from '../agent.js'
4
2
  import type { SessionManager } from '../session-manager.js'
5
3
  import { saveState } from '../state.js'
6
- import { OverviewUI } from './overview.js'
7
4
  import { DetailUI } from './detail.js'
5
+ import { OverviewUI } from './overview.js'
6
+ import type { KeyInfo } from './terminal.js'
7
+ import { TerminalUI } from './terminal.js'
8
8
 
9
9
  type Mode = 'overview' | 'detail'
10
10
 
11
11
  export class App {
12
- private screen: Widgets.Screen
13
12
  private manager: SessionManager
14
13
  private statePath: string
14
+ private terminal: TerminalUI
15
15
  private overviewUI: OverviewUI
16
16
  private detailUI: DetailUI
17
17
  private mode: Mode = 'overview'
18
18
 
19
- constructor(manager: SessionManager, statePath: string) {
19
+ constructor(manager: SessionManager, statePath: string, terminal = new TerminalUI()) {
20
20
  this.manager = manager
21
21
  this.statePath = statePath
22
+ this.terminal = terminal
22
23
 
23
- this.screen = blessed.screen({
24
- smartCSR: true,
25
- title: 'mav',
26
- fullUnicode: true,
27
- })
28
-
29
- this.overviewUI = new OverviewUI(this.screen, manager, (session) => {
24
+ this.overviewUI = new OverviewUI(terminal, manager, (session) => {
30
25
  if (session) {
31
26
  this.switchToDetail(session)
32
27
  }
33
28
  })
34
- this.detailUI = new DetailUI(this.screen, () => {
29
+ this.detailUI = new DetailUI(terminal, () => {
35
30
  if (this.mode === 'detail') {
36
31
  this.switchToOverview()
37
32
  }
38
33
  })
39
34
 
40
- const proto = Object.getPrototypeOf(this.screen) as { render?: () => void }
41
- const baseRender = proto.render ?? this.screen.render
42
- const protoRender = baseRender.bind(this.screen)
43
- ;(this.screen as unknown as { render: () => void }).render = () => {
44
- if (this.mode === 'detail') return
45
- protoRender()
46
- }
47
-
48
35
  this.bindGlobalKeys()
49
36
 
50
- this.screen.on('resize', () => {
51
- const cols = this.screen.width as number
52
- const rows = this.screen.height as number
37
+ this.terminal.onResize(() => {
38
+ const cols = this.terminal.cols
39
+ const rows = this.terminal.rows
53
40
  if (this.mode === 'detail') {
54
41
  this.detailUI.resize(cols, rows)
55
42
  return
56
43
  }
57
44
 
58
45
  this.overviewUI.resizeSelectedSession()
59
- this.screen.render()
60
46
  })
61
47
  }
62
48
 
63
49
  private bindGlobalKeys(): void {
64
- this.screen.key('q', () => {
65
- if (this.mode === 'detail') return
66
- this.shutdown()
67
- })
68
-
69
- this.screen.key('C-c', () => {
50
+ this.terminal.onKeypress((str, key) => {
70
51
  if (this.mode === 'overview') {
71
- this.shutdown()
52
+ this.handleOverviewKeypress(str, key)
72
53
  }
73
54
  })
55
+ }
74
56
 
75
- this.screen.key(['right', 'enter'], () => {
76
- if (this.mode !== 'overview') return
77
- if (this.overviewUI.isPromptOpen()) return
57
+ private handleOverviewKeypress(str: string, key: KeyInfo): void {
58
+ if (key.ctrl && key.name === 'c') {
59
+ this.shutdown()
60
+ return
61
+ }
62
+
63
+ if (str === 'q') {
64
+ this.shutdown()
65
+ return
66
+ }
67
+
68
+ if (!this.overviewUI.isPromptOpen() && (key.name === 'right' || key.name === 'enter' || key.name === 'return')) {
78
69
  const session = this.manager.selectedSession
79
70
  if (!session) return
80
- this.switchToDetail()
81
- })
71
+ this.switchToDetail(session)
72
+ return
73
+ }
74
+
75
+ this.overviewUI.handleKeypress(str, key)
82
76
  }
83
77
 
84
78
  private switchToDetail(session: AgentSession | null = this.manager.selectedSession): void {
85
79
  if (!session) return
86
80
  this.mode = 'detail'
87
81
  this.overviewUI.hide()
88
- this.screen.program.normalBuffer()
89
- this.screen.program.disableMouse()
90
- this.screen.realloc()
82
+ this.terminal.exitAlternateScreen()
91
83
  this.detailUI.attach(session)
92
- this.detailUI.resize(this.screen.width as number, this.screen.height as number)
84
+ this.detailUI.resize(this.terminal.cols, this.terminal.rows)
93
85
  this.detailUI.show()
94
86
  }
95
87
 
@@ -97,9 +89,7 @@ export class App {
97
89
  this.mode = 'overview'
98
90
  this.detailUI.detach()
99
91
  this.detailUI.hide()
100
- this.screen.program.alternateBuffer()
101
- this.screen.program.enableMouse()
102
- this.screen.realloc()
92
+ this.terminal.enterAlternateScreen()
103
93
  this.overviewUI.show()
104
94
  }
105
95
 
@@ -110,13 +100,13 @@ export class App {
110
100
  // 終了シーケンスは継続する
111
101
  }
112
102
  this.manager.killAll()
113
- this.screen.destroy()
103
+ this.terminal.destroy()
114
104
  process.exit(0)
115
105
  }
116
106
 
117
107
  start(): void {
108
+ this.terminal.enterAlternateScreen()
118
109
  this.overviewUI.show()
119
110
  this.overviewUI.resizeSelectedSession()
120
- this.screen.render()
121
111
  }
122
112
  }
package/src/ui/detail.ts CHANGED
@@ -1,10 +1,12 @@
1
1
  import { appendFileSync } from 'node:fs'
2
- import type { Widgets } from 'neo-blessed'
3
2
  import type { AgentSession } from '../agent.js'
3
+ import type { TerminalUI } from './terminal.js'
4
4
 
5
5
  export class DetailUI {
6
6
  private static readonly KEYBOARD_FLAGS_RESPONSE_PATTERN = /\x1b\[\?(\d+)u/g
7
7
  private static readonly PENDING_INPUT_FLUSH_DELAY_MS = 25
8
+ private static readonly EXIT_HINT =
9
+ '\x1b[7m mav detail \x1b[27m Press Ctrl+] to return to Overview\r\n\r\n'
8
10
 
9
11
  private static readonly EXIT_SEQUENCES = [
10
12
  '\x1d',
@@ -16,24 +18,24 @@ export class DetailUI {
16
18
  /^\x1b\[99;5u$/,
17
19
  ] as const
18
20
 
19
- private screen: Widgets.Screen
21
+ private terminal: TerminalUI
20
22
  private onExitDetail: () => void
21
23
  private currentSession: AgentSession | null = null
22
24
  private dataListener: ((data: string) => void) | null = null
23
- private rawInputListener: ((chunk: unknown) => void) | null = null
25
+ private rawInputDisposer: (() => void) | null = null
24
26
  private pendingInput = ''
25
27
  private pendingInputTimer: ReturnType<typeof setTimeout> | null = null
26
28
  private keyboardEnhancementFlags: string | null = null
27
29
 
28
- constructor(screen: Widgets.Screen, onExitDetail: () => void) {
29
- this.screen = screen
30
+ constructor(terminal: TerminalUI, onExitDetail: () => void) {
31
+ this.terminal = terminal
30
32
  this.onExitDetail = onExitDetail
31
33
  }
32
34
 
33
35
  private isExitShortcut(input: string): boolean {
34
36
  return (
35
37
  DetailUI.EXIT_SEQUENCES.includes(
36
- input as (typeof DetailUI.EXIT_SEQUENCES)[number]
38
+ input as (typeof DetailUI.EXIT_SEQUENCES)[number]
37
39
  ) ||
38
40
  DetailUI.EXIT_SEQUENCE_PATTERNS.some((pattern) => pattern.test(input))
39
41
  )
@@ -84,25 +86,24 @@ export class DetailUI {
84
86
  this.currentSession = session
85
87
  this.pendingInput = ''
86
88
 
87
- const { input, output } = this.screen.program
88
-
89
89
  const safeLog = session.logBuffer
90
90
  .join('')
91
91
  .replace(/\x1b\[\?104[79][hl]|\x1b\[\?47[hl]/g, '')
92
92
  .replace(/\x1b\[(?:>?\d*c|\?u|>q|\?\d+\$p)/g, '')
93
93
  if (this.keyboardEnhancementFlags) {
94
- output.write(`\x1b[=${this.keyboardEnhancementFlags}u`)
94
+ this.terminal.write(`\x1b[=${this.keyboardEnhancementFlags}u`)
95
95
  }
96
- output.write('\x1b[?1l')
97
- output.write('\x1b[H\x1b[2J')
98
- output.write(safeLog)
96
+ this.terminal.write('\x1b[?1l')
97
+ this.terminal.clearScreen()
98
+ this.terminal.write(DetailUI.EXIT_HINT)
99
+ this.terminal.write(safeLog)
99
100
 
100
101
  this.dataListener = (data: string) => {
101
- output.write(data)
102
+ this.terminal.write(data)
102
103
  }
103
104
  session.on('data', this.dataListener)
104
105
 
105
- this.rawInputListener = (chunk: unknown) => {
106
+ this.rawInputDisposer = this.terminal.onData((chunk: string | Buffer) => {
106
107
  const str = Buffer.isBuffer(chunk)
107
108
  ? chunk.toString('utf8')
108
109
  : typeof chunk === 'string'
@@ -134,8 +135,7 @@ export class DetailUI {
134
135
  }
135
136
 
136
137
  this.flushPendingInput()
137
- }
138
- input.on('data', this.rawInputListener)
138
+ })
139
139
  }
140
140
 
141
141
  detach(): void {
@@ -143,12 +143,10 @@ export class DetailUI {
143
143
  this.currentSession.off('data', this.dataListener)
144
144
  this.dataListener = null
145
145
  }
146
- if (this.rawInputListener) {
147
- this.screen.program.input.removeListener('data', this.rawInputListener)
148
- this.rawInputListener = null
149
- }
146
+ this.rawInputDisposer?.()
147
+ this.rawInputDisposer = null
150
148
  if (this.keyboardEnhancementFlags) {
151
- this.screen.program.output.write('\x1b[=0u')
149
+ this.terminal.write('\x1b[=0u')
152
150
  }
153
151
  this.clearPendingInputTimer()
154
152
  this.pendingInput = ''