@k1e1n04/mav 0.1.23 → 0.1.25
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/README.md +26 -3
- package/bin/mav.ts +31 -0
- package/dist/bin/mav.js +27 -0
- package/dist/bin/mav.js.map +1 -1
- package/dist/package.json +1 -1
- package/dist/src/agent-launch.js +3 -0
- package/dist/src/agent-launch.js.map +1 -1
- package/dist/src/agent.d.ts +9 -1
- package/dist/src/agent.js +25 -2
- package/dist/src/agent.js.map +1 -1
- package/dist/src/hook-injector.d.ts +22 -0
- package/dist/src/hook-injector.js +101 -0
- package/dist/src/hook-injector.js.map +1 -0
- package/dist/src/index.js +49 -15
- package/dist/src/index.js.map +1 -1
- package/dist/src/ipc-server.d.ts +13 -0
- package/dist/src/ipc-server.js +58 -0
- package/dist/src/ipc-server.js.map +1 -0
- package/dist/src/session-manager.d.ts +2 -1
- package/dist/src/session-manager.js +2 -2
- package/dist/src/session-manager.js.map +1 -1
- package/dist/src/ui/app.d.ts +1 -1
- package/dist/src/ui/app.js +2 -2
- package/dist/src/ui/app.js.map +1 -1
- package/dist/src/ui/overview.d.ts +4 -1
- package/dist/src/ui/overview.js +85 -5
- package/dist/src/ui/overview.js.map +1 -1
- package/package.json +1 -1
- package/src/agent-launch.ts +3 -0
- package/src/agent.ts +33 -2
- package/src/hook-injector.ts +131 -0
- package/src/index.ts +56 -15
- package/src/ipc-server.ts +72 -0
- package/src/session-manager.ts +3 -2
- package/src/ui/app.ts +2 -1
- package/src/ui/overview.ts +91 -4
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
import { writeFileSync, readFileSync, existsSync, unlinkSync, mkdirSync } from 'node:fs'
|
|
2
|
+
import { join } from 'node:path'
|
|
3
|
+
import { homedir } from 'node:os'
|
|
4
|
+
import { randomUUID } from 'node:crypto'
|
|
5
|
+
|
|
6
|
+
export interface HookInjectionResult {
|
|
7
|
+
/** Injected startup args */
|
|
8
|
+
args: string[]
|
|
9
|
+
/** Temp file paths to delete on agent exit */
|
|
10
|
+
hookFiles: string[]
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
interface BuildHookArgsOptions {
|
|
14
|
+
/** Agent working directory (for gemini-cli) */
|
|
15
|
+
cwd?: string
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Injects a hook into the startup args for the given agent type and returns the result.
|
|
20
|
+
* Generated temp files are listed in hookFiles.
|
|
21
|
+
* Call cleanupHookFiles() after the agent exits.
|
|
22
|
+
*/
|
|
23
|
+
export function buildHookArgs(
|
|
24
|
+
agentType: string,
|
|
25
|
+
baseArgs: string[],
|
|
26
|
+
hookCommand: string,
|
|
27
|
+
options: BuildHookArgsOptions = {},
|
|
28
|
+
): HookInjectionResult {
|
|
29
|
+
switch (agentType) {
|
|
30
|
+
case 'claude-code':
|
|
31
|
+
return buildClaudeCodeHook(baseArgs, hookCommand)
|
|
32
|
+
case 'gemini-cli':
|
|
33
|
+
return buildGeminiHook(baseArgs, hookCommand, options.cwd)
|
|
34
|
+
case 'codex':
|
|
35
|
+
return buildCodexHook(baseArgs, hookCommand)
|
|
36
|
+
default:
|
|
37
|
+
return { args: baseArgs, hookFiles: [] }
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function buildClaudeCodeHook(baseArgs: string[], hookCommand: string): HookInjectionResult {
|
|
42
|
+
const settings = {
|
|
43
|
+
hooks: {
|
|
44
|
+
PostToolUse: [
|
|
45
|
+
{
|
|
46
|
+
matcher: '',
|
|
47
|
+
hooks: [{ type: 'command', command: hookCommand }],
|
|
48
|
+
},
|
|
49
|
+
],
|
|
50
|
+
},
|
|
51
|
+
}
|
|
52
|
+
return {
|
|
53
|
+
args: [...baseArgs, '--settings', JSON.stringify(settings)],
|
|
54
|
+
hookFiles: [],
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const MAV_MARKER = '_mavGenerated'
|
|
59
|
+
|
|
60
|
+
function buildGeminiHook(
|
|
61
|
+
baseArgs: string[],
|
|
62
|
+
hookCommand: string,
|
|
63
|
+
cwd?: string,
|
|
64
|
+
): HookInjectionResult {
|
|
65
|
+
const projectDir = cwd ?? process.cwd()
|
|
66
|
+
const geminiDir = join(projectDir, '.gemini')
|
|
67
|
+
const settingsPath = join(geminiDir, 'settings.local.json')
|
|
68
|
+
|
|
69
|
+
// If the file exists and was NOT written by mav, skip to protect user's config
|
|
70
|
+
if (existsSync(settingsPath)) {
|
|
71
|
+
try {
|
|
72
|
+
const existing = JSON.parse(readFileSync(settingsPath, 'utf-8')) as Record<string, unknown>
|
|
73
|
+
if (!existing[MAV_MARKER]) {
|
|
74
|
+
// User-owned file — do not overwrite
|
|
75
|
+
return { args: baseArgs, hookFiles: [] }
|
|
76
|
+
}
|
|
77
|
+
} catch {
|
|
78
|
+
// Unreadable/invalid JSON — treat as user-owned and skip
|
|
79
|
+
return { args: baseArgs, hookFiles: [] }
|
|
80
|
+
}
|
|
81
|
+
// File was written by mav (stale from crash) — overwrite below
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const settings = {
|
|
85
|
+
[MAV_MARKER]: true,
|
|
86
|
+
hooks: {
|
|
87
|
+
AfterTool: [
|
|
88
|
+
{ matcher: '', hooks: [{ type: 'command', command: hookCommand }] },
|
|
89
|
+
],
|
|
90
|
+
},
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
mkdirSync(geminiDir, { recursive: true })
|
|
94
|
+
writeFileSync(settingsPath, JSON.stringify(settings, null, 2))
|
|
95
|
+
|
|
96
|
+
return { args: baseArgs, hookFiles: [settingsPath] }
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function buildCodexHook(baseArgs: string[], hookCommand: string): HookInjectionResult {
|
|
100
|
+
const codexHome = process.env.CODEX_HOME ?? join(homedir(), '.codex')
|
|
101
|
+
mkdirSync(codexHome, { recursive: true })
|
|
102
|
+
const profileName = `mav-hook-${randomUUID()}`
|
|
103
|
+
const tomlPath = join(codexHome, `${profileName}.config.toml`)
|
|
104
|
+
|
|
105
|
+
const tomlContent = [
|
|
106
|
+
'[hooks.post_tool_use]',
|
|
107
|
+
`cmd = ${JSON.stringify(hookCommand)}`,
|
|
108
|
+
'timeout_seconds = 10',
|
|
109
|
+
].join('\n')
|
|
110
|
+
|
|
111
|
+
writeFileSync(tomlPath, tomlContent)
|
|
112
|
+
|
|
113
|
+
return {
|
|
114
|
+
args: [...baseArgs, '--profile-v2', profileName],
|
|
115
|
+
hookFiles: [tomlPath],
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Deletes temp files generated by buildHookArgs.
|
|
121
|
+
* Call this when the agent process exits.
|
|
122
|
+
*/
|
|
123
|
+
export function cleanupHookFiles(hookFiles: string[]): void {
|
|
124
|
+
for (const f of hookFiles) {
|
|
125
|
+
try {
|
|
126
|
+
if (existsSync(f)) unlinkSync(f)
|
|
127
|
+
} catch {
|
|
128
|
+
// Ignore deletion failures (file may already be gone)
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { homedir } from 'node:os'
|
|
1
|
+
import { homedir, tmpdir } from 'node:os'
|
|
2
2
|
import { join, dirname } from 'node:path'
|
|
3
3
|
import { loadConfig } from './config.js'
|
|
4
4
|
import { clearCurrentSessionState, saveCurrentSessionState } from './current-session.js'
|
|
@@ -7,6 +7,8 @@ import { SessionManager } from './session-manager.js'
|
|
|
7
7
|
import { App } from './ui/app.js'
|
|
8
8
|
import type { AgentSession } from './agent.js'
|
|
9
9
|
import { resolveSessionArgs } from './agent-launch.js'
|
|
10
|
+
import { createServer } from './ipc-server.js'
|
|
11
|
+
import { buildHookArgs } from './hook-injector.js'
|
|
10
12
|
|
|
11
13
|
export interface StartOptions {
|
|
12
14
|
configPath?: string
|
|
@@ -31,8 +33,16 @@ export function start(options: StartOptions = {}): SessionManager {
|
|
|
31
33
|
|
|
32
34
|
const savedState = loadState(statePath)
|
|
33
35
|
|
|
36
|
+
// Start IPC server for cwd tracking (non-blocking)
|
|
37
|
+
const socketPath = join(tmpdir(), `mav-${process.pid}.sock`)
|
|
38
|
+
const ipcServer = createServer(socketPath)
|
|
39
|
+
ipcServer.listen().catch((err: unknown) => {
|
|
40
|
+
const msg = err instanceof Error ? err.message : String(err)
|
|
41
|
+
process.stderr.write(`mav: IPC server failed to start (${msg}), cwd tracking disabled\n`)
|
|
42
|
+
})
|
|
43
|
+
|
|
34
44
|
const manager = new SessionManager()
|
|
35
|
-
const app = new App(manager, statePath, undefined, config.agents)
|
|
45
|
+
const app = new App(manager, statePath, undefined, config.agents, socketPath)
|
|
36
46
|
const publishSelectedSession = () => {
|
|
37
47
|
const session = manager.selectedSession
|
|
38
48
|
if (!session) {
|
|
@@ -64,11 +74,20 @@ export function start(options: StartOptions = {}): SessionManager {
|
|
|
64
74
|
)
|
|
65
75
|
|
|
66
76
|
const restoredCwd = savedSession?.cwd
|
|
67
|
-
const
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
77
|
+
const hookCmd = `mav report cwd "$(pwd)"`
|
|
78
|
+
let hookedArgs = args
|
|
79
|
+
let hookFiles: string[] = []
|
|
80
|
+
try {
|
|
81
|
+
const hookResult = buildHookArgs(agentConfig.type, args, hookCmd, { cwd: restoredCwd })
|
|
82
|
+
hookedArgs = hookResult.args
|
|
83
|
+
hookFiles = hookResult.hookFiles
|
|
84
|
+
} catch {
|
|
85
|
+
// hook file I/O failed — start agent without hooks
|
|
86
|
+
}
|
|
87
|
+
const session = manager.addSession(
|
|
88
|
+
{ ...agentConfig, args: hookedArgs, ...(restoredCwd != null && { cwd: restoredCwd }) },
|
|
89
|
+
{ socketPath, hookFiles },
|
|
90
|
+
) as AgentSession & { sessionId?: string }
|
|
72
91
|
session.baseArgs = agentConfig.args
|
|
73
92
|
configSessionIds.add(session.id)
|
|
74
93
|
|
|
@@ -89,12 +108,20 @@ export function start(options: StartOptions = {}): SessionManager {
|
|
|
89
108
|
savedSession.sessionId,
|
|
90
109
|
true,
|
|
91
110
|
)
|
|
92
|
-
const
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
111
|
+
const hookCmd = `mav report cwd "$(pwd)"`
|
|
112
|
+
let hookedArgs = args
|
|
113
|
+
let hookFiles: string[] = []
|
|
114
|
+
try {
|
|
115
|
+
const hookResult = buildHookArgs(rc.type, args, hookCmd, { cwd: savedSession.cwd })
|
|
116
|
+
hookedArgs = hookResult.args
|
|
117
|
+
hookFiles = hookResult.hookFiles
|
|
118
|
+
} catch {
|
|
119
|
+
// hook file I/O failed — start agent without hooks
|
|
120
|
+
}
|
|
121
|
+
const session = manager.addSession(
|
|
122
|
+
{ type: rc.type, cmd: rc.cmd, args: hookedArgs, ...(savedSession.cwd != null && { cwd: savedSession.cwd }) },
|
|
123
|
+
{ socketPath, hookFiles },
|
|
124
|
+
) as AgentSession & { sessionId?: string }
|
|
98
125
|
session.baseArgs = rc.args
|
|
99
126
|
if (newSessionId != null) {
|
|
100
127
|
session.sessionId = newSessionId
|
|
@@ -126,14 +153,28 @@ export function start(options: StartOptions = {}): SessionManager {
|
|
|
126
153
|
publishSelectedSession()
|
|
127
154
|
})
|
|
128
155
|
|
|
156
|
+
manager.on('name', (sessionId: string) => {
|
|
157
|
+
if (manager.selectedSession?.id !== sessionId) {
|
|
158
|
+
return
|
|
159
|
+
}
|
|
160
|
+
publishSelectedSession()
|
|
161
|
+
})
|
|
162
|
+
|
|
129
163
|
publishSelectedSession()
|
|
130
164
|
|
|
165
|
+
ipcServer.onMessage((msg) => {
|
|
166
|
+
if (msg.type === 'cwd') {
|
|
167
|
+
const session = manager.sessions.find((s) => s.id === msg.sessionId)
|
|
168
|
+
session?.notifyCwd(msg.path)
|
|
169
|
+
}
|
|
170
|
+
})
|
|
171
|
+
|
|
131
172
|
// q/Ctrl+C 以外の終了(ウィンドウ閉じ等)でも state を保存する
|
|
132
173
|
const saveOnExit = () => {
|
|
133
174
|
try { saveState(statePath, manager) } catch { /* ignore */ }
|
|
134
175
|
}
|
|
135
|
-
process.once('SIGTERM', () => { saveOnExit(); process.exit(0) })
|
|
136
|
-
process.once('SIGHUP', () => { saveOnExit(); process.exit(0) })
|
|
176
|
+
process.once('SIGTERM', () => { ipcServer.close(); saveOnExit(); process.exit(0) })
|
|
177
|
+
process.once('SIGHUP', () => { ipcServer.close(); saveOnExit(); process.exit(0) })
|
|
137
178
|
|
|
138
179
|
app.start()
|
|
139
180
|
return manager
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { createServer as netCreateServer, Server, Socket } from 'node:net'
|
|
2
|
+
import { existsSync, unlinkSync } from 'node:fs'
|
|
3
|
+
|
|
4
|
+
export interface IpcMessage {
|
|
5
|
+
type: string
|
|
6
|
+
sessionId: string
|
|
7
|
+
path: string
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
type MessageHandler = (msg: IpcMessage) => void
|
|
11
|
+
|
|
12
|
+
export interface IpcServer {
|
|
13
|
+
listen(): Promise<void>
|
|
14
|
+
onMessage(handler: MessageHandler): void
|
|
15
|
+
close(): void
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function createServer(socketPath: string): IpcServer {
|
|
19
|
+
const handlers: MessageHandler[] = []
|
|
20
|
+
let server: Server | null = null
|
|
21
|
+
|
|
22
|
+
return {
|
|
23
|
+
onMessage(handler) {
|
|
24
|
+
handlers.push(handler)
|
|
25
|
+
},
|
|
26
|
+
|
|
27
|
+
listen() {
|
|
28
|
+
return new Promise((resolve, reject) => {
|
|
29
|
+
if (existsSync(socketPath)) {
|
|
30
|
+
unlinkSync(socketPath)
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
server = netCreateServer((socket: Socket) => {
|
|
34
|
+
let buf = ''
|
|
35
|
+
socket.on('error', () => {}) // absorb socket errors (e.g. ECONNRESET)
|
|
36
|
+
socket.on('data', (chunk) => {
|
|
37
|
+
buf += chunk.toString()
|
|
38
|
+
const lines = buf.split('\n')
|
|
39
|
+
buf = lines.pop() ?? ''
|
|
40
|
+
for (const line of lines) {
|
|
41
|
+
if (!line.trim()) continue
|
|
42
|
+
try {
|
|
43
|
+
const msg = JSON.parse(line) as IpcMessage
|
|
44
|
+
if (msg.type && msg.sessionId && typeof msg.path === 'string') {
|
|
45
|
+
handlers.forEach((h) => h(msg))
|
|
46
|
+
}
|
|
47
|
+
} catch {
|
|
48
|
+
// ignore
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
})
|
|
52
|
+
})
|
|
53
|
+
|
|
54
|
+
server.once('error', reject)
|
|
55
|
+
server.listen(socketPath, () => {
|
|
56
|
+
server!.removeListener('error', reject)
|
|
57
|
+
server!.on('error', () => {}) // absorb post-bind server errors
|
|
58
|
+
resolve()
|
|
59
|
+
})
|
|
60
|
+
})
|
|
61
|
+
},
|
|
62
|
+
|
|
63
|
+
close() {
|
|
64
|
+
const closingServer = server
|
|
65
|
+
server = null
|
|
66
|
+
if (existsSync(socketPath)) {
|
|
67
|
+
try { unlinkSync(socketPath) } catch { /* ignore */ }
|
|
68
|
+
}
|
|
69
|
+
closingServer?.close()
|
|
70
|
+
},
|
|
71
|
+
}
|
|
72
|
+
}
|
package/src/session-manager.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { EventEmitter } from 'node:events'
|
|
2
2
|
import { AgentSession } from './agent.js'
|
|
3
3
|
import type { AgentConfig } from './config.js'
|
|
4
|
+
import type { IpcContext } from './agent.js'
|
|
4
5
|
import type { MavState } from './state.js'
|
|
5
6
|
|
|
6
7
|
type SessionListeners = {
|
|
@@ -20,8 +21,8 @@ export class SessionManager extends EventEmitter {
|
|
|
20
21
|
this.emit('selection', this.selectedSession)
|
|
21
22
|
}
|
|
22
23
|
|
|
23
|
-
addSession(config: AgentConfig, cols = 80, rows = 24): AgentSession {
|
|
24
|
-
const session = new AgentSession(config, cols, rows)
|
|
24
|
+
addSession(config: AgentConfig, ipcContext?: IpcContext, cols = 80, rows = 24): AgentSession {
|
|
25
|
+
const session = new AgentSession(config, cols, rows, ipcContext)
|
|
25
26
|
|
|
26
27
|
const onData = (chunk: string) => { this.emit('data', session.id, chunk) }
|
|
27
28
|
const onExit = (code: number) => { this.emit('exit', session.id, code) }
|
package/src/ui/app.ts
CHANGED
|
@@ -22,6 +22,7 @@ export class App {
|
|
|
22
22
|
statePath: string,
|
|
23
23
|
terminal = new TerminalUI(),
|
|
24
24
|
agentConfigs: AgentConfig[] = [],
|
|
25
|
+
socketPath?: string,
|
|
25
26
|
) {
|
|
26
27
|
this.manager = manager
|
|
27
28
|
this.statePath = statePath
|
|
@@ -31,7 +32,7 @@ export class App {
|
|
|
31
32
|
if (session) {
|
|
32
33
|
this.switchToDetail(session)
|
|
33
34
|
}
|
|
34
|
-
}, agentConfigs)
|
|
35
|
+
}, agentConfigs, socketPath)
|
|
35
36
|
this.detailUI = new DetailUI(terminal, () => {
|
|
36
37
|
if (this.mode === 'detail') {
|
|
37
38
|
this.switchToOverview()
|
package/src/ui/overview.ts
CHANGED
|
@@ -2,12 +2,14 @@ import type { SessionManager } from '../session-manager.js'
|
|
|
2
2
|
import type { AgentSession } from '../agent.js'
|
|
3
3
|
import type { AgentConfig } from '../config.js'
|
|
4
4
|
import { getAgentDefaults, resolveSessionArgs } from '../agent-launch.js'
|
|
5
|
+
import { buildHookArgs } from '../hook-injector.js'
|
|
5
6
|
import { completePath } from './path-completion.js'
|
|
6
7
|
import type { KeyInfo, TerminalUI } from './terminal.js'
|
|
7
8
|
|
|
8
9
|
type PromptState =
|
|
9
10
|
| { mode: 'agent'; selectedIndex: number }
|
|
10
11
|
| { mode: 'cwd'; agentType: string; value: string; candidates: string[] }
|
|
12
|
+
| { mode: 'rename'; sessionId: string; value: string }
|
|
11
13
|
| { mode: 'error'; message: string }
|
|
12
14
|
| null
|
|
13
15
|
|
|
@@ -32,12 +34,13 @@ export class OverviewUI {
|
|
|
32
34
|
{ status: 'error', label: 'Failed' },
|
|
33
35
|
] as const
|
|
34
36
|
|
|
35
|
-
private static readonly AGENT_TYPES = ['claude-code', 'codex', 'gemini-cli', 'copilot'] as const
|
|
37
|
+
private static readonly AGENT_TYPES = ['claude-code', 'codex', 'gemini-cli', 'copilot', 'cursor', 'opencode', 'antigravity-cli'] as const
|
|
36
38
|
|
|
37
39
|
private terminal: TerminalUI
|
|
38
40
|
private manager: SessionManager
|
|
39
41
|
private onSessionCreated?: (session: SessionManager['selectedSession']) => void
|
|
40
42
|
private agentConfigs: AgentConfig[]
|
|
43
|
+
private socketPath: string | undefined
|
|
41
44
|
private promptState: PromptState = null
|
|
42
45
|
private displaySessionIds: string[] = []
|
|
43
46
|
private visible = false
|
|
@@ -47,11 +50,13 @@ export class OverviewUI {
|
|
|
47
50
|
manager: SessionManager,
|
|
48
51
|
onSessionCreated?: (session: SessionManager['selectedSession']) => void,
|
|
49
52
|
agentConfigs: AgentConfig[] = [],
|
|
53
|
+
socketPath?: string,
|
|
50
54
|
) {
|
|
51
55
|
this.terminal = terminal
|
|
52
56
|
this.manager = manager
|
|
53
57
|
this.onSessionCreated = onSessionCreated
|
|
54
58
|
this.agentConfigs = agentConfigs
|
|
59
|
+
this.socketPath = socketPath
|
|
55
60
|
|
|
56
61
|
this.syncList()
|
|
57
62
|
|
|
@@ -93,6 +98,10 @@ export class OverviewUI {
|
|
|
93
98
|
this.handleCwdPromptKeypress(key)
|
|
94
99
|
return
|
|
95
100
|
}
|
|
101
|
+
if (this.promptState?.mode === 'rename') {
|
|
102
|
+
this.handleRenamePromptKeypress(key)
|
|
103
|
+
return
|
|
104
|
+
}
|
|
96
105
|
if (this.promptState?.mode === 'error') {
|
|
97
106
|
if (key.name === 'enter' || key.name === 'return' || key.name === 'escape' || str === 'q') {
|
|
98
107
|
this.promptState = null
|
|
@@ -116,6 +125,11 @@ export class OverviewUI {
|
|
|
116
125
|
return
|
|
117
126
|
}
|
|
118
127
|
|
|
128
|
+
if (str === 'e') {
|
|
129
|
+
this.showRenamePrompt()
|
|
130
|
+
return
|
|
131
|
+
}
|
|
132
|
+
|
|
119
133
|
if (str === 'd' || (key.ctrl && key.name === 'x')) {
|
|
120
134
|
const session = this.manager.selectedSession
|
|
121
135
|
if (session) {
|
|
@@ -202,12 +216,62 @@ export class OverviewUI {
|
|
|
202
216
|
}
|
|
203
217
|
}
|
|
204
218
|
|
|
219
|
+
private handleRenamePromptKeypress(key: KeyInfo): void {
|
|
220
|
+
const state = this.promptState
|
|
221
|
+
if (!state || state.mode !== 'rename') return
|
|
222
|
+
|
|
223
|
+
if (key.name === 'escape') {
|
|
224
|
+
this.promptState = null
|
|
225
|
+
this.render()
|
|
226
|
+
return
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
if (key.name === 'enter' || key.name === 'return') {
|
|
230
|
+
const session = this.manager.sessions.find((candidate) => candidate.id === state.sessionId)
|
|
231
|
+
const value = state.value.trim()
|
|
232
|
+
this.promptState = null
|
|
233
|
+
if (session && value.length >= 3) {
|
|
234
|
+
if ('setDisplayName' in session && typeof session.setDisplayName === 'function') {
|
|
235
|
+
session.setDisplayName(value)
|
|
236
|
+
} else {
|
|
237
|
+
session.displayName = value
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
this.syncList()
|
|
241
|
+
this.render()
|
|
242
|
+
return
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
if (key.name === 'backspace') {
|
|
246
|
+
state.value = state.value.slice(0, -1)
|
|
247
|
+
this.render()
|
|
248
|
+
return
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
if (!key.ctrl && !key.meta && key.sequence?.length === 1) {
|
|
252
|
+
state.value += key.sequence
|
|
253
|
+
this.render()
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
|
|
205
257
|
private showAddPrompt(): void {
|
|
206
258
|
if (this.promptState) return
|
|
207
259
|
this.promptState = { mode: 'agent', selectedIndex: 0 }
|
|
208
260
|
this.render()
|
|
209
261
|
}
|
|
210
262
|
|
|
263
|
+
private showRenamePrompt(): void {
|
|
264
|
+
if (this.promptState) return
|
|
265
|
+
const session = this.manager.selectedSession
|
|
266
|
+
if (!session) return
|
|
267
|
+
this.promptState = {
|
|
268
|
+
mode: 'rename',
|
|
269
|
+
sessionId: session.id,
|
|
270
|
+
value: session.displayName ?? session.id,
|
|
271
|
+
}
|
|
272
|
+
this.render()
|
|
273
|
+
}
|
|
274
|
+
|
|
211
275
|
private spawnAgent(agentType: string, cwd: string): void {
|
|
212
276
|
const config = this.agentConfigs.find((candidate) => candidate.type === agentType)
|
|
213
277
|
const defaults = config ?? {
|
|
@@ -215,12 +279,27 @@ export class OverviewUI {
|
|
|
215
279
|
...getAgentDefaults(agentType),
|
|
216
280
|
}
|
|
217
281
|
const { args, newSessionId } = resolveSessionArgs(agentType, defaults.args, undefined, false)
|
|
282
|
+
|
|
283
|
+
let hookedArgs = args
|
|
284
|
+
let hookFiles: string[] = []
|
|
285
|
+
if (this.socketPath) {
|
|
286
|
+
try {
|
|
287
|
+
const hookCmd = `mav report cwd "$(pwd)"`
|
|
288
|
+
const hookResult = buildHookArgs(agentType, args, hookCmd, { cwd })
|
|
289
|
+
hookedArgs = hookResult.args
|
|
290
|
+
hookFiles = hookResult.hookFiles
|
|
291
|
+
} catch {
|
|
292
|
+
// hook injection failed — start agent without hooks
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
const ipcContext = this.socketPath ? { socketPath: this.socketPath, hookFiles } : undefined
|
|
218
297
|
const session = this.manager.addSession({
|
|
219
298
|
type: agentType,
|
|
220
299
|
cmd: defaults.cmd,
|
|
221
|
-
args,
|
|
300
|
+
args: hookedArgs,
|
|
222
301
|
cwd,
|
|
223
|
-
}) as AgentSession & { sessionId?: string }
|
|
302
|
+
}, ipcContext) as AgentSession & { sessionId?: string }
|
|
224
303
|
session.baseArgs = defaults.args
|
|
225
304
|
if (newSessionId != null) {
|
|
226
305
|
session.sessionId = newSessionId
|
|
@@ -550,7 +629,7 @@ export class OverviewUI {
|
|
|
550
629
|
}
|
|
551
630
|
|
|
552
631
|
lines.push(this.color(this.makeRule('controls', width), OverviewUI.ANSI.dim))
|
|
553
|
-
lines.push(...this.wrapPlainText('↑/↓ or j/k move Enter detail Ctrl+] back n new d delete q quit', width))
|
|
632
|
+
lines.push(...this.wrapPlainText('↑/↓ or j/k move Enter detail Ctrl+] back n new e rename d delete q quit', width))
|
|
554
633
|
return this.finalizeLines(lines)
|
|
555
634
|
}
|
|
556
635
|
|
|
@@ -586,6 +665,14 @@ export class OverviewUI {
|
|
|
586
665
|
return this.finalizeLines(lines)
|
|
587
666
|
}
|
|
588
667
|
|
|
668
|
+
if (state.mode === 'rename') {
|
|
669
|
+
lines.push(this.fitPlain('rename session', width))
|
|
670
|
+
lines.push(...this.wrapPlainText(state.value, width))
|
|
671
|
+
lines.push('')
|
|
672
|
+
lines.push(...this.wrapPlainText('Enter: confirm Esc: cancel', width))
|
|
673
|
+
return this.finalizeLines(lines)
|
|
674
|
+
}
|
|
675
|
+
|
|
589
676
|
lines.push(this.fitPlain('Error', width))
|
|
590
677
|
for (const line of state.message.split('\n')) {
|
|
591
678
|
lines.push(this.fitPlain(line, width))
|