feat: tighten freestyle discovery and analyst handoff

This commit is contained in:
2026-07-26 10:21:56 +04:00
parent cf9eecc895
commit 516af1ca96
7 changed files with 198 additions and 31 deletions
+42 -4
View File
@@ -367,13 +367,22 @@ func (m *Model) applyServer(msg protocol.ServerMessage) {
if s := m.session(msg.SessionID); s != nil {
// The resolved gate names no tool on the wire; recover it from the pending
// queue before dropping the gate, for the inline row.
tool := ""
tool, preview := "", ""
for _, a := range s.PendingQueue {
if a.RequestID == msg.RequestID {
tool = a.ToolName
preview = a.Preview
break
}
}
// Extract the target (file path for diffs, command for shell) from the
// preview so the action row reads "approved file_write → healthcheck.sh".
target := ""
if tool == "file_write" && isUnifiedDiff(preview) {
target = diffTarget(preview)
} else if tool == "shell" && strings.HasPrefix(preview, "{") {
target = previewTarget(preview)
}
// Drop just the resolved gate; any others stay queued and the band
// advances to the next rather than vanishing entirely.
s.removeApproval(msg.RequestID)
@@ -383,7 +392,7 @@ func (m *Model) applyServer(msg protocol.ServerMessage) {
}
s.addEvent(msg.OccurredAt, "ApprovalResolved", detail)
s.LastEventAt = nowMillis()
m.appendAction(msg.SessionID, approvalIcon(msg.Outcome), approvalActionText(msg.Outcome, tool, msg.Reason))
m.appendAction(msg.SessionID, approvalIcon(msg.Outcome), approvalActionText(msg.Outcome, tool, target, msg.Reason))
}
case protocol.TypeSessionSnapshot:
m.onSnapshot(msg)
@@ -580,11 +589,13 @@ func lastToolParams(s *Session, name string) []string {
}
// paramSuffix renders pretty call args as a " (k=v · k=v)" suffix, or "" when there are none.
// The clip is generous (200 cols) so real tool arguments like shell commands, grep patterns,
// and file paths are legible — the panel width is the real limit.
func paramSuffix(params []string) string {
if len(params) == 0 {
return ""
}
return " (" + clip(strings.Join(params, " · "), 56) + ")"
return " (" + clip(strings.Join(params, " · "), 200) + ")"
}
// actionToolText joins a tool label with a short, clipped result summary.
@@ -608,7 +619,7 @@ func approvalIcon(outcome string) string {
// approvalActionText renders the inline approval row, noting an auto-approval that fired via a
// standing grant (reason "grant:<id>").
func approvalActionText(outcome, tool, reason string) string {
func approvalActionText(outcome, tool, target, reason string) string {
verb := "approved"
switch outcome {
case "REJECTED":
@@ -620,12 +631,39 @@ func approvalActionText(outcome, tool, reason string) string {
if tool != "" {
txt = verb + " " + tool
}
if target != "" {
txt += " → " + target
}
if strings.HasPrefix(reason, "grant:") {
txt += " · via grant"
}
return txt
}
// previewTarget extracts a short target label from a JSON preview payload
// (e.g. {"argv":["bash","-c","curl ..."]} → the last argv element, truncated).
func previewTarget(preview string) string {
// Naive extraction: find "argv" array and return the last element
if i := strings.Index(preview, `"argv":`); i >= 0 {
rest := preview[i+len(`"argv":`):]
if j := strings.IndexByte(rest, '['); j >= 0 {
argv := rest[j:]
if k := strings.IndexByte(argv, ']'); k >= 0 {
argv = argv[:k+1]
}
// Parse comma-separated strings, grab the last non-empty
parts := strings.Split(strings.Trim(argv, "[]"), ",")
for i := len(parts) - 1; i >= 0; i-- {
candidate := strings.Trim(strings.Trim(parts[i], ` "`), `"`)
if candidate != "" {
return clip(candidate, 40)
}
}
}
}
return ""
}
// onSessionAnnounced fills in a session's workflow identity (the announce is the
// only event carrying workflowId) and applies auto-focus. The session entry itself
// was already created by the auto-vivify path in applyServer.