Skip to content

Commit 534f9c0

Browse files
committed
making RPC mode more responsive and stable for Devtool clients
1 parent b00243c commit 534f9c0

7 files changed

Lines changed: 849 additions & 4 deletions

File tree

src/index.ts

Lines changed: 36 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -138,8 +138,16 @@ program
138138
.option('--unrestricted', 'Run without any approval prompts (use with caution)', false)
139139
.option('--restricted', 'Deny all dangerous operations automatically', false)
140140
.option('--auto-skill', 'Auto-generate skills based on project analysis', false)
141+
.option('--skill-install [skill-name]', 'Install a community skill (opens browser if no name)')
142+
.option('--project', 'Install skill to project level (with --skill-install)', false)
141143
.option('--mode <mode>', 'Run mode: interactive (default) or rpc', 'interactive')
142-
.action(async (opts: CLIOptions & { mode?: string }) => {
144+
.action(async (opts: CLIOptions & { mode?: string; skillInstall?: string | boolean; project?: boolean }) => {
145+
// Handle --skill-install flag
146+
if (opts.skillInstall !== undefined) {
147+
await runSkillInstall(opts);
148+
return;
149+
}
150+
143151
if (opts.mode === 'rpc') {
144152
await runRpcMode(opts);
145153
} else {
@@ -327,4 +335,31 @@ function printWelcome(runtime: AgentRuntime, authUser?: AuthUser): void {
327335
console.log();
328336
}
329337

338+
/**
339+
* Handle --skill-install flag for installing community skills
340+
*/
341+
async function runSkillInstall(opts: CLIOptions & { skillInstall?: string | boolean; project?: boolean }): Promise<void> {
342+
const config = await loadConfig(opts.config);
343+
const workspaceRoot = resolveWorkspaceRoot(config, opts.path);
344+
345+
// Import skill install dependencies
346+
const { SkillsRegistry } = await import('./skills/SkillsRegistry.js');
347+
const { AUTOHAND_PATHS } = await import('./constants.js');
348+
const { skillsInstall } = await import('./commands/skills-install.js');
349+
350+
// Initialize skills registry
351+
const skillsRegistry = new SkillsRegistry(AUTOHAND_PATHS.skills);
352+
await skillsRegistry.initialize();
353+
await skillsRegistry.setWorkspace(workspaceRoot);
354+
355+
// Determine skill name (if provided)
356+
const skillName = typeof opts.skillInstall === 'string' ? opts.skillInstall : undefined;
357+
358+
// Run the install command
359+
await skillsInstall({
360+
skillsRegistry,
361+
workspaceRoot,
362+
}, skillName);
363+
}
364+
330365
program.parseAsync();

src/modes/rpc/adapter.ts

Lines changed: 62 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ export class RPCAdapter {
4343
private imageManager: ImageManager | null = null;
4444
private sessionId: string | null = null;
4545
private currentTurnId: string | null = null;
46+
private turnStartTime: number | null = null;
4647
private currentMessageId: string | null = null;
4748
private currentMessageContent = '';
4849
private pendingPermissions = new Map<string, PendingPermission>();
@@ -139,6 +140,7 @@ export class RPCAdapter {
139140

140141
// Start a new turn
141142
this.currentTurnId = generateId('turn');
143+
this.turnStartTime = Date.now();
142144
writeNotification(RPC_NOTIFICATIONS.TURN_START, {
143145
turnId: this.currentTurnId,
144146
timestamp: createTimestamp(),
@@ -243,7 +245,49 @@ export class RPCAdapter {
243245
try {
244246
// Debug: log instruction being executed
245247
process.stderr.write(`[RPC DEBUG] Executing instruction: ${instruction.substring(0, 100)}\n`);
246-
success = await this.agent.runInstruction(instruction);
248+
249+
// Check if it's a slash command and handle it directly
250+
if (this.agent.isSlashCommand(instruction)) {
251+
const { command, args } = this.agent.parseSlashCommand(instruction);
252+
process.stderr.write(`[RPC DEBUG] Handling slash command: ${command}, args: ${JSON.stringify(args)}\n`);
253+
254+
// First check if the command is supported
255+
if (this.agent.isSlashCommandSupported(command)) {
256+
const result = await this.agent.handleSlashCommand(command, args);
257+
if (result !== null) {
258+
// Slash command returned data
259+
this.currentMessageContent = result;
260+
writeNotification(RPC_NOTIFICATIONS.MESSAGE_UPDATE, {
261+
messageId: this.currentMessageId,
262+
delta: result,
263+
timestamp: createTimestamp(),
264+
});
265+
} else {
266+
// Command was handled but returned null (output went to console)
267+
// This is success - the command was executed
268+
this.currentMessageContent = `Command ${command} executed.`;
269+
writeNotification(RPC_NOTIFICATIONS.MESSAGE_UPDATE, {
270+
messageId: this.currentMessageId,
271+
delta: this.currentMessageContent,
272+
timestamp: createTimestamp(),
273+
});
274+
}
275+
success = true;
276+
} else {
277+
// Command not found
278+
this.currentMessageContent = `Unknown command: ${command}. Type /help for available commands.`;
279+
writeNotification(RPC_NOTIFICATIONS.MESSAGE_UPDATE, {
280+
messageId: this.currentMessageId,
281+
delta: this.currentMessageContent,
282+
timestamp: createTimestamp(),
283+
});
284+
success = false;
285+
}
286+
} else {
287+
// Not a slash command - run as regular instruction via LLM
288+
success = await this.agent.runInstruction(instruction);
289+
}
290+
247291
process.stderr.write(`[RPC DEBUG] Instruction completed, success=${success}, content length=${this.currentMessageContent.length}\n`);
248292
} catch (err) {
249293
const errorMessage = err instanceof Error ? err.message : String(err);
@@ -268,29 +312,39 @@ export class RPCAdapter {
268312
timestamp: createTimestamp(),
269313
});
270314

271-
// End turn with context percent
315+
// End turn with stats
316+
const durationMs = this.turnStartTime ? Date.now() - this.turnStartTime : undefined;
317+
const snapshot = this.agent?.getStatusSnapshot();
272318
writeNotification(RPC_NOTIFICATIONS.TURN_END, {
273319
turnId: this.currentTurnId!,
274320
timestamp: createTimestamp(),
275321
contextPercent: this.contextPercent,
322+
tokensUsed: snapshot?.tokensUsed,
323+
durationMs,
276324
});
277325

278326
this.status = 'idle';
279327
this.currentTurnId = null;
328+
this.turnStartTime = null;
280329
this.currentMessageId = null;
281330
this.abortController = null;
282331

283332
return { success };
284333
} catch (error) {
285-
// End turn on error with context percent
334+
// End turn on error with stats
335+
const durationMs = this.turnStartTime ? Date.now() - this.turnStartTime : undefined;
336+
const snapshot = this.agent?.getStatusSnapshot();
286337
writeNotification(RPC_NOTIFICATIONS.TURN_END, {
287338
turnId: this.currentTurnId!,
288339
timestamp: createTimestamp(),
289340
contextPercent: this.contextPercent,
341+
tokensUsed: snapshot?.tokensUsed,
342+
durationMs,
290343
});
291344

292345
this.status = 'idle';
293346
this.currentTurnId = null;
347+
this.turnStartTime = null;
294348
this.currentMessageId = null;
295349
this.abortController = null;
296350

@@ -330,15 +384,20 @@ export class RPCAdapter {
330384

331385
// End turn if one is in progress
332386
if (this.currentTurnId) {
387+
const durationMs = this.turnStartTime ? Date.now() - this.turnStartTime : undefined;
388+
const snapshot = this.agent?.getStatusSnapshot();
333389
writeNotification(RPC_NOTIFICATIONS.TURN_END, {
334390
turnId: this.currentTurnId,
335391
timestamp: createTimestamp(),
336392
contextPercent: this.contextPercent,
393+
tokensUsed: snapshot?.tokensUsed,
394+
durationMs,
337395
});
338396
}
339397

340398
// Reset state
341399
this.currentTurnId = null;
400+
this.turnStartTime = null;
342401
this.currentMessageId = null;
343402
this.currentMessageContent = '';
344403
this.abortController = null;

src/modes/rpc/types.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -228,6 +228,9 @@ export interface TurnStartParams {
228228
export interface TurnEndParams {
229229
turnId: string;
230230
timestamp: string;
231+
tokensUsed?: number;
232+
durationMs?: number;
233+
contextPercent?: number;
231234
}
232235

233236
export interface MessageStartParams {

0 commit comments

Comments
 (0)