@@ -362,11 +362,19 @@ func (*RootCmd) mcpConfigureCursor() *serpent.Command {
362
362
}
363
363
364
364
type taskReport struct {
365
- link string
366
- messageID int64
365
+ // link is optional.
366
+ link string
367
+ // messageID must be set if this update is from a *user* message. A user
368
+ // message only happens when interacting via the AI AgentAPI (as opposed to
369
+ // interacting with the terminal directly).
370
+ messageID * int64
371
+ // selfReported must be set if the update is directly from the AI agent
372
+ // (as opposed to the screen watcher).
367
373
selfReported bool
368
- state codersdk.WorkspaceAppStatusState
369
- summary string
374
+ // state must always be set.
375
+ state codersdk.WorkspaceAppStatusState
376
+ // summary is optional.
377
+ summary string
370
378
}
371
379
372
380
type mcpServer struct {
@@ -388,31 +396,48 @@ func (r *RootCmd) mcpServer() *serpent.Command {
388
396
return & serpent.Command {
389
397
Use : "server" ,
390
398
Handler : func (inv * serpent.Invocation ) error {
391
- // lastUserMessageID is the ID of the last *user* message that we saw. A
392
- // user message only happens when interacting via the AI AgentAPI (as
393
- // opposed to interacting with the terminal directly).
394
- var lastUserMessageID int64
395
399
var lastReport taskReport
396
400
// Create a queue that skips duplicates and preserves summaries.
397
401
queue := cliutil.NewQueue [taskReport ](512 ).WithPredicate (func (report taskReport ) (taskReport , bool ) {
398
- // Use "working" status if this is a new user message. If this is not a
399
- // new user message, and the status is "working" and not self-reported
400
- // (meaning it came from the screen watcher), then it means one of two
401
- // things:
402
- // 1. The AI agent is still working, so there is nothing to update.
403
- // 2. The AI agent stopped working, then the user has interacted with
404
- // the terminal directly. For now, we are ignoring these updates.
405
- // This risks missing cases where the user manually submits a new
406
- // prompt and the AI agent becomes active and does not update itself,
407
- // but it avoids spamming useless status updates as the user is
408
- // typing, so the tradeoff is worth it. In the future, if we can
409
- // reliably distinguish between user and AI agent activity, we can
410
- // change this.
411
- if report .messageID > lastUserMessageID {
412
- report .state = codersdk .WorkspaceAppStatusStateWorking
413
- } else if report .state == codersdk .WorkspaceAppStatusStateWorking && ! report .selfReported {
402
+ // Avoid queuing empty statuses (this would probably indicate a
403
+ // developer error)
404
+ if report .state == "" {
414
405
return report , false
415
406
}
407
+ // If this is a user message, discard if it is not new.
408
+ if report .messageID != nil && lastReport .messageID != nil &&
409
+ * lastReport .messageID >= * report .messageID {
410
+ return report , false
411
+ }
412
+ // If this is not a user message, and the status is "working" and not
413
+ // self-reported (meaning it came from the screen watcher), then it
414
+ // means one of two things:
415
+ //
416
+ // 1. The AI agent is not working; the user is interacting with the
417
+ // terminal directly.
418
+ // 2. The AI agent is working.
419
+ //
420
+ // At the moment, we have no way to tell the difference between these
421
+ // two states. In the future, if we can reliably distinguish between
422
+ // user and AI agent activity, we can change this.
423
+ //
424
+ // If this is our first update, we assume it is the AI agent working and
425
+ // accept the update.
426
+ //
427
+ // Otherwise we discard the update. This risks missing cases where the
428
+ // user manually submits a new prompt and the AI agent becomes active
429
+ // (and does not update itself), but it avoids spamming useless status
430
+ // updates as the user is typing, so the tradeoff is worth it.
431
+ if report .messageID == nil &&
432
+ report .state == codersdk .WorkspaceAppStatusStateWorking &&
433
+ ! report .selfReported && lastReport .state != "" {
434
+ return report , false
435
+ }
436
+ // Keep track of the last message ID so we can tell when a message is
437
+ // new or if it has been re-emitted.
438
+ if report .messageID == nil {
439
+ report .messageID = lastReport .messageID
440
+ }
416
441
// Preserve previous message and URI if there was no message.
417
442
if report .summary == "" {
418
443
report .summary = lastReport .summary
@@ -600,7 +625,8 @@ func (s *mcpServer) startWatcher(ctx context.Context, inv *serpent.Invocation) {
600
625
case agentapi.EventMessageUpdate :
601
626
if ev .Role == agentapi .RoleUser {
602
627
err := s .queue .Push (taskReport {
603
- messageID : ev .Id ,
628
+ messageID : & ev .Id ,
629
+ state : codersdk .WorkspaceAppStatusStateWorking ,
604
630
})
605
631
if err != nil {
606
632
cliui .Warnf (inv .Stderr , "Failed to queue update: %s" , err )
@@ -650,10 +676,18 @@ func (s *mcpServer) startServer(ctx context.Context, inv *serpent.Invocation, in
650
676
// Add tool dependencies.
651
677
toolOpts := []func (* toolsdk.Deps ){
652
678
toolsdk .WithTaskReporter (func (args toolsdk.ReportTaskArgs ) error {
679
+ // The agent does not reliably report its status correctly. If AgentAPI
680
+ // is enabled, we will always set the status to "working" when we get an
681
+ // MCP message, and rely on the screen watcher to eventually catch the
682
+ // idle state.
683
+ state := codersdk .WorkspaceAppStatusStateWorking
684
+ if s .aiAgentAPIClient == nil {
685
+ state = codersdk .WorkspaceAppStatusState (args .State )
686
+ }
653
687
return s .queue .Push (taskReport {
654
688
link : args .Link ,
655
689
selfReported : true ,
656
- state : codersdk . WorkspaceAppStatusState ( args . State ) ,
690
+ state : state ,
657
691
summary : args .Summary ,
658
692
})
659
693
}),
0 commit comments