feat: show live Kimi activity on agent cards

This commit is contained in:
Emil
2026-07-18 23:50:04 +03:00
parent ed71c3cd43
commit 7feaaf4e0e
2 changed files with 34 additions and 5 deletions
+29 -1
View File
@@ -288,6 +288,8 @@ sealed class KimiAgent : IDisposable
public string Role { get; }
public IReadOnlyList<string> Scopes { get; }
public string State { get; private set; } = "starting";
public string Activity { get; private set; } = "Starting ACP session";
public DateTimeOffset ActivityUpdatedAt { get; private set; } = DateTimeOffset.UtcNow;
public KimiAgent(string name, string workspace, string role, IReadOnlyList<string> scopes, FleetOptions options, Action<string, string, string> publish)
{
@@ -319,6 +321,7 @@ sealed class KimiAgent : IDisposable
public async Task StartAsync()
{
SetActivity("Initializing ACP session");
if (!_process.Start()) throw new InvalidOperationException($"Could not start Kimi process for '{Name}'.");
_process.Exited += (_, _) => { State = "stopped"; _publish(Name, "exit", $"Kimi exited with {_process.ExitCode}."); };
_ = ReadOutputAsync();
@@ -333,6 +336,7 @@ sealed class KimiAgent : IDisposable
var session = await RequestAsync("session/new", new { cwd = Workspace, mcpServers = Array.Empty<object>() });
_sessionId = session.GetProperty("sessionId").GetString() ?? throw new InvalidOperationException("ACP did not return sessionId.");
State = "ready";
SetActivity("Ready for a task");
_publish(Name, "ready", $"Role={Role}; scopes={string.Join(", ", Scopes)}");
}
@@ -341,16 +345,19 @@ sealed class KimiAgent : IDisposable
if (string.IsNullOrWhiteSpace(prompt)) throw new InvalidOperationException("Prompt must not be empty.");
if (_sessionId is null) throw new InvalidOperationException($"Agent '{Name}' has no ACP session.");
State = "working";
SetActivity("Planning assigned task");
_publish(Name, "prompt", prompt.Length > 160 ? prompt[..160] + "…" : prompt);
try
{
await RequestAsync("session/prompt", new { sessionId = _sessionId, prompt = new[] { new { type = "text", text = prompt } } });
State = "ready";
SetActivity("Ready for a task");
_publish(Name, "completed", "Prompt completed.");
}
catch (Exception ex)
{
State = "error";
SetActivity("Needs attention");
_publish(Name, "error", ex.Message);
}
}
@@ -406,10 +413,13 @@ sealed class KimiAgent : IDisposable
if (methodName == "session/request_permission" && root.TryGetProperty("id", out var permissionId) && root.TryGetProperty("params", out var permission))
{
var optionId = SelectApproval(permission);
SetActivity("Approving requested action");
_ = RespondToPermissionAsync(permissionId.GetRawText(), optionId);
_publish(Name, "permission", $"Auto-approved '{optionId}' (yolo mode).");
return;
}
if (methodName == "session/update" && root.TryGetProperty("params", out var updateParams))
UpdateActivityFromSessionUpdate(updateParams);
_publish(Name, methodName, root.TryGetProperty("params", out var p) ? p.GetRawText() : string.Empty);
}
else _publish(Name, "protocol", line);
@@ -417,6 +427,24 @@ sealed class KimiAgent : IDisposable
catch (JsonException) { _publish(Name, "stdout", line); }
}
private void UpdateActivityFromSessionUpdate(JsonElement parameters)
{
if (!parameters.TryGetProperty("update", out var update) || !update.TryGetProperty("sessionUpdate", out var typeNode)) return;
var type = typeNode.GetString();
if ((type == "tool_call" || type == "tool_call_update") && update.TryGetProperty("title", out var title) && !string.IsNullOrWhiteSpace(title.GetString()))
SetActivity(title.GetString()!);
else if (type == "agent_thought_chunk" && !Activity.StartsWith("Reading", StringComparison.OrdinalIgnoreCase) && !Activity.StartsWith("Running", StringComparison.OrdinalIgnoreCase))
SetActivity("Analyzing task");
else if (type == "agent_message_chunk")
SetActivity("Preparing a report");
}
private void SetActivity(string value)
{
Activity = value;
ActivityUpdatedAt = DateTimeOffset.UtcNow;
}
private static string SelectApproval(JsonElement permission)
{
if (!permission.TryGetProperty("options", out var options) || options.ValueKind != JsonValueKind.Array)
@@ -446,7 +474,7 @@ sealed class KimiAgent : IDisposable
finally { _stdinLock.Release(); }
}
public object Snapshot() => new { name = Name, workspace = Workspace, role = Role, scopes = Scopes, state = State, sessionId = _sessionId };
public object Snapshot() => new { name = Name, workspace = Workspace, role = Role, scopes = Scopes, state = State, activity = Activity, activityUpdatedAt = ActivityUpdatedAt, sessionId = _sessionId };
public void Dispose()
{