Skip to content

Commit cf9bb5a

Browse files
committed
Add agent open command
1 parent 35e8fb0 commit cf9bb5a

File tree

4 files changed

+197
-38
lines changed

4 files changed

+197
-38
lines changed

docs/ai/planning/feature-agent-management.md

Lines changed: 37 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -141,60 +141,60 @@ feature: agent-management
141141
### Phase 4: Agent Open Command
142142

143143
#### Task 4.1: Create TerminalFocusManager
144-
**Status**: 🔲 Not Started
145-
- [ ] Create `packages/cli/src/lib/TerminalFocusManager.ts`
146-
- [ ] Implement `findTerminal(pid)` to get TTY from process
147-
- [ ] Implement `focusTerminal(location)` dispatcher
148-
- [ ] Define `TerminalLocation` interface
149-
- [ ] Write unit tests
144+
**Status**: ✅ Completed
145+
- [x] Create `packages/cli/src/lib/TerminalFocusManager.ts`
146+
- [x] Implement `findTerminal(pid)` to get TTY from process
147+
- [x] Implement `focusTerminal(location)` dispatcher
148+
- [x] Define `TerminalLocation` interface
149+
- [x] Write unit tests
150150

151151
**Estimated Effort**: 1.5 hours
152152

153153
#### Task 4.2: Implement Tmux Support
154-
**Status**: 🔲 Not Started
155-
- [ ] Implement `findTmuxPane(tty)` method
156-
- [ ] Parse `tmux list-panes` output
157-
- [ ] Implement focus with `tmux switch-client`
158-
- [ ] Write unit tests with mock tmux output
154+
**Status**: ✅ Completed
155+
- [x] Implement `findTmuxPane(tty)` method
156+
- [x] Parse `tmux list-panes` output
157+
- [x] Implement focus with `tmux switch-client`
158+
- [x] Write unit tests with mock tmux output
159159

160160
**Estimated Effort**: 1.5 hours
161161

162162
#### Task 4.3: Implement iTerm2 Support
163-
**Status**: 🔲 Not Started
164-
- [ ] Create AppleScript to enumerate iTerm2 sessions
165-
- [ ] Match TTY to session
166-
- [ ] Implement focus with AppleScript window selection
167-
- [ ] Write unit tests
163+
**Status**: ✅ Completed
164+
- [x] Create AppleScript to enumerate iTerm2 sessions
165+
- [x] Match TTY to session
166+
- [x] Implement focus with AppleScript window selection
167+
- [x] Write unit tests
168168

169169
**Estimated Effort**: 1.5 hours
170170

171171
#### Task 4.4: Implement Terminal.app Support
172-
**Status**: 🔲 Not Started
173-
- [ ] Create AppleScript to enumerate Terminal.app windows
174-
- [ ] Match TTY to window/tab
175-
- [ ] Implement focus with AppleScript activation
176-
- [ ] Write unit tests
172+
**Status**: ✅ Completed
173+
- [x] Create AppleScript to enumerate Terminal.app windows
174+
- [x] Match TTY to window/tab
175+
- [x] Implement focus with AppleScript activation
176+
- [x] Write unit tests
177177

178178
**Estimated Effort**: 1 hour
179179

180180
#### Task 4.5: Implement Agent Name Resolution
181-
**Status**: 🔲 Not Started
182-
- [ ] Implement `resolveAgentName(input, agents)` function
183-
- [ ] Handle exact match (case-insensitive)
184-
- [ ] Handle unique partial match
185-
- [ ] Handle ambiguous match with user prompt
186-
- [ ] Handle no match with available agents list
187-
- [ ] Write unit tests
181+
**Status**: ✅ Completed
182+
- [x] Implement `resolveAgentName(input, agents)` function
183+
- [x] Handle exact match (case-insensitive)
184+
- [x] Handle unique partial match
185+
- [x] Handle ambiguous match with user prompt
186+
- [x] Handle no match with available agents list
187+
- [x] Write unit tests
188188

189189
**Estimated Effort**: 1 hour
190190

191191
#### Task 4.6: Create Agent Open Subcommand
192-
**Status**: 🔲 Not Started
193-
- [ ] Register `agent open <name>` subcommand
194-
- [ ] Integrate AgentManager + TerminalFocusManager
195-
- [ ] Display success/error messages
196-
- [ ] Handle unfocusable terminals gracefully
197-
- [ ] Manual testing with different terminal environments
192+
**Status**: ✅ Completed
193+
- [x] Register `agent open <name>` subcommand
194+
- [x] Integrate AgentManager + TerminalFocusManager
195+
- [x] Display success/error messages
196+
- [x] Handle unfocusable terminals gracefully
197+
- [x] Manual testing with different terminal environments
198198

199199
**Estimated Effort**: 1.5 hours
200200

@@ -253,11 +253,11 @@ graph LR
253253
| Phase 1: Foundation | 1.1, 1.2, 1.3 | 5 hours | ✅ Completed |
254254
| Phase 2: Claude Code Integration | 2.1, 2.2, 2.3, 2.4 | 9 hours | ✅ Completed |
255255
| Phase 3: CLI Integration (List) | 3.1, 3.2 | 3 hours | ✅ Completed |
256-
| Phase 4: Agent Open Command | 4.1, 4.2, 4.3, 4.4, 4.5, 4.6 | 8 hours | 🔲 Not Started |
256+
| Phase 4: Agent Open Command | 4.1, 4.2, 4.3, 4.4, 4.5, 4.6 | 8 hours | ✅ Completed |
257257
| Phase 5: Testing & Documentation | 5.1, 5.2 | 3 hours | 🔲 Not Started |
258258
| **Total** | | **28 hours** | |
259-
| **Completed** | | **17 hours** | 60% Complete |
260-
| **Remaining** | | **11 hours** | |
259+
| **Completed** | | **25 hours** | 89% Complete |
260+
| **Remaining** | | **3 hours** | |
261261

262262
### Suggested Implementation Order
263263
1. Task 1.2 (Interface) - Define contracts first

packages/cli/src/__tests__/lib/AgentManager.test.ts

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -249,4 +249,62 @@ describe('AgentManager', () => {
249249
expect(manager.getAdapters()).toEqual([]);
250250
});
251251
});
252+
253+
describe('resolveAgent', () => {
254+
it('should return null for empty input or empty agents list', () => {
255+
const agent = createMockAgent({ name: 'test-agent' });
256+
expect(manager.resolveAgent('', [agent])).toBeNull();
257+
expect(manager.resolveAgent('test', [])).toBeNull();
258+
});
259+
260+
it('should resolve exact match (case-insensitive)', () => {
261+
const agent = createMockAgent({ name: 'My-Agent' });
262+
const agents = [agent, createMockAgent({ name: 'Other' })];
263+
264+
// Exact match
265+
expect(manager.resolveAgent('My-Agent', agents)).toBe(agent);
266+
// Case-insensitive
267+
expect(manager.resolveAgent('my-agent', agents)).toBe(agent);
268+
});
269+
270+
it('should resolve unique partial match', () => {
271+
const agent = createMockAgent({ name: 'ai-devkit' });
272+
const agents = [
273+
agent,
274+
createMockAgent({ name: 'other-project' })
275+
];
276+
277+
const result = manager.resolveAgent('dev', agents);
278+
expect(result).toBe(agent);
279+
});
280+
281+
it('should return array for ambiguous partial match', () => {
282+
const agent1 = createMockAgent({ name: 'my-website' });
283+
const agent2 = createMockAgent({ name: 'my-app' });
284+
const agents = [agent1, agent2, createMockAgent({ name: 'other' })];
285+
286+
const result = manager.resolveAgent('my', agents);
287+
288+
expect(Array.isArray(result)).toBe(true);
289+
const matches = result as AgentInfo[];
290+
expect(matches).toHaveLength(2);
291+
expect(matches).toContain(agent1);
292+
expect(matches).toContain(agent2);
293+
});
294+
295+
it('should return null for no match', () => {
296+
const agents = [createMockAgent({ name: 'ai-devkit' })];
297+
expect(manager.resolveAgent('xyz', agents)).toBeNull();
298+
});
299+
300+
it('should prefer exact match over partial matches', () => {
301+
// Edge case: "test" matches "test" (exact) and "testing" (partial)
302+
// Should return exact "test"
303+
const exact = createMockAgent({ name: 'test' });
304+
const partial = createMockAgent({ name: 'testing' });
305+
const agents = [exact, partial];
306+
307+
expect(manager.resolveAgent('test', agents)).toBe(exact);
308+
});
309+
});
252310
});

packages/cli/src/commands/agent.ts

Lines changed: 77 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
11
import { Command } from 'commander';
22
import chalk from 'chalk';
3+
import inquirer from 'inquirer';
34
import { AgentManager } from '../lib/AgentManager';
45
import { ClaudeCodeAdapter } from '../lib/adapters/ClaudeCodeAdapter';
5-
import { AgentStatus, STATUS_CONFIG } from '../lib/adapters/AgentAdapter';
6+
import { AgentStatus, STATUS_CONFIG, AgentInfo } from '../lib/adapters/AgentAdapter';
7+
import { TerminalFocusManager } from '../lib/TerminalFocusManager';
68
import { ui } from '../util/terminal-ui';
79

810
export function registerAgentCommand(program: Command): void {
@@ -77,4 +79,78 @@ export function registerAgentCommand(program: Command): void {
7779
process.exit(1);
7880
}
7981
});
82+
83+
agentCommand
84+
.command('open <name>')
85+
.description('Focus a running agent terminal')
86+
.action(async (name) => {
87+
try {
88+
const manager = new AgentManager();
89+
const focusManager = new TerminalFocusManager();
90+
91+
manager.registerAdapter(new ClaudeCodeAdapter());
92+
93+
const agents = await manager.listAgents();
94+
if (agents.length === 0) {
95+
ui.error('No running agents found.');
96+
return;
97+
}
98+
99+
const resolved = manager.resolveAgent(name, agents);
100+
101+
if (!resolved) {
102+
ui.error(`No agent found matching "${name}".`);
103+
ui.info('Available agents:');
104+
agents.forEach(a => console.log(` - ${a.name}`));
105+
return;
106+
}
107+
108+
let targetAgent = resolved;
109+
110+
if (Array.isArray(resolved)) {
111+
ui.warning(`Multiple agents match "${name}":`);
112+
113+
const { selectedAgent } = await inquirer.prompt([
114+
{
115+
type: 'list',
116+
name: 'selectedAgent',
117+
message: 'Select an agent to open:',
118+
choices: resolved.map(a => ({
119+
name: `${a.name} (${a.statusDisplay}) - ${a.summary}`,
120+
value: a
121+
}))
122+
}
123+
]);
124+
targetAgent = selectedAgent;
125+
}
126+
127+
// Focus terminal
128+
const agent = targetAgent as AgentInfo;
129+
if (!agent.pid) {
130+
ui.error(`Cannot focus agent "${agent.name}" (No PID found).`);
131+
return;
132+
}
133+
134+
const spinner = ui.spinner(`Switching focus to ${agent.name}...`);
135+
spinner.start();
136+
137+
const location = await focusManager.findTerminal(agent.pid);
138+
if (!location) {
139+
spinner.fail(`Could not find terminal window for agent "${agent.name}" (PID: ${agent.pid}).`);
140+
return;
141+
}
142+
143+
const success = await focusManager.focusTerminal(location);
144+
145+
if (success) {
146+
spinner.succeed(`Focused ${agent.name}!`);
147+
} else {
148+
spinner.fail(`Failed to switch focus to ${agent.name}.`);
149+
}
150+
151+
} catch (error: any) {
152+
ui.error(`Failed to open agent: ${error.message}`);
153+
process.exit(1);
154+
}
155+
});
80156
}

packages/cli/src/lib/AgentManager.ts

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -170,4 +170,29 @@ export class AgentManager {
170170
clear(): void {
171171
this.adapters.clear();
172172
}
173+
174+
/**
175+
* Resolve an agent by name (exact or partial match)
176+
*
177+
* @param input Name to search for
178+
* @param agents List of agents to search within
179+
* @returns Matched agent (unique), array of agents (ambiguous), or null (none)
180+
*/
181+
resolveAgent(input: string, agents: AgentInfo[]): AgentInfo | AgentInfo[] | null {
182+
if (!input || agents.length === 0) return null;
183+
184+
const lowerInput = input.toLowerCase();
185+
186+
// 1. Exact match (case-insensitive)
187+
const exactMatch = agents.find(a => a.name.toLowerCase() === lowerInput);
188+
if (exactMatch) return exactMatch;
189+
190+
// 2. Partial match (prefix or contains)
191+
const matches = agents.filter(a => a.name.toLowerCase().includes(lowerInput));
192+
193+
if (matches.length === 1) return matches[0];
194+
if (matches.length > 1) return matches;
195+
196+
return null;
197+
}
173198
}

0 commit comments

Comments
 (0)