Improve the transcript: tool grouping, report auto-ack, context meter, spinner, Enter to send

This commit is contained in:
Ian Gulliver
2026-10-14 16:03:46 -07:00
parent ca2644374d
commit ab4c3d2c6f
19 changed files with 734 additions and 92 deletions
+3 -1
View File
@@ -170,12 +170,14 @@ func (m *Manager) validateAsk(ctx context.Context, l *live, args askArgs) error
return nil
}
func (m *Manager) fileReport(l *live, r report.Report) {
func (m *Manager) fileReport(l *live, r report.Report) string {
id := store.NewID()
err := m.store.CreateInboxItem(context.Background(), m.newItem(l, id, id, api.Report, api.Fyi, false, store.ToPayload(r)))
if err != nil {
slog.Error("file report", "session", l.id, "err", err)
return ""
}
return id
}
func (m *Manager) fileError(l *live, msg string) {
+33 -3
View File
@@ -59,6 +59,10 @@ type live struct {
stopping bool
interrupted bool
rateLimited map[string]any
contextModel string
contextUsed int
contextWindow int
}
func New(st *store.Store, limits Limits) *Manager {
@@ -455,11 +459,12 @@ func (m *Manager) ensureLive(ctx context.Context, sess api.AgentSession, task ap
l = &live{id: sess.Id, vendor: sess.Vendor, taskID: task.Id, projectID: task.ProjectId}
l.gate = &report.Gate{OnReport: func(r report.Report) {
err := m.emit(context.Background(), l, agentActor, "agent.report", store.ToPayload(r))
payload := store.ToPayload(r)
payload["inbox_item_id"] = m.fileReport(l, r)
err := m.emit(context.Background(), l, agentActor, "agent.report", payload)
if err != nil {
slog.Error("record report", "session", l.id, "err", err)
}
m.fileReport(l, r)
}}
opts := claude.Options{
@@ -549,7 +554,15 @@ func (m *Manager) handle(ctx context.Context, l *live, ev claude.Event) error {
return err
}
}
return nil
usage, err := ev.ContextUsage()
if err != nil {
return err
}
if usage.Tokens == 0 {
return nil
}
l.contextModel = usage.Model
return m.updateContext(ctx, l, usage.Tokens, l.contextWindow)
case "user":
blocks, err := ev.Blocks()
@@ -585,6 +598,14 @@ func (m *Manager) handle(ctx context.Context, l *live, ev claude.Event) error {
return err
}
window := res.ModelUsage[l.contextModel].ContextWindow
if window > 0 {
err = m.updateContext(ctx, l, l.contextUsed, window)
if err != nil {
return err
}
}
m.mu.Lock()
interrupted := l.interrupted
rateLimited := l.rateLimited
@@ -644,6 +665,15 @@ func (m *Manager) handle(ctx context.Context, l *live, ev claude.Event) error {
return m.emit(ctx, l, agentActor, "agent.vendor", raw)
}
func (m *Manager) updateContext(ctx context.Context, l *live, used, window int) error {
if used == l.contextUsed && window == l.contextWindow {
return nil
}
l.contextUsed = used
l.contextWindow = window
return m.emit(ctx, l, agentActor, "session.context", map[string]any{"model": l.contextModel, "used": used, "window": window})
}
func (m *Manager) handleAssistantBlock(ctx context.Context, l *live, b claude.Block) error {
switch b.Type {
case "text":
+11 -1
View File
@@ -156,12 +156,22 @@ func TestTurnAndFollowUp(t *testing.T) {
}
events := e.waitIdle()
for _, kind := range []string{"session.created", "user.message", "agent.tool_call", "agent.tool_result", "agent.report", "turn.ended"} {
for _, kind := range []string{"session.created", "user.message", "agent.tool_call", "agent.tool_result", "agent.report", "turn.ended", "session.context"} {
if !has(events, kind) {
t.Fatalf("missing %s in %v", kind, kinds(events))
}
}
window := 0
for _, ev := range events {
if ev.Kind == "session.context" {
window = ev.Payload["window"].(int)
}
}
if window <= 0 {
t.Fatalf("no context window reported after the turn ended")
}
log := gitOut(t, e.task.WorktreePath, "log", "-1", "--format=%an <%ae>")
if log != gitName+" <"+gitEmail+">" {
t.Fatalf("agent committed as %q", log)