7de788ceed
* feat: add cross-integration entity references * fix(web): anchor entity reference menu to composer * fix: keep Kandev tasks under @ mentions * fix: address entity reference review findings * test(e2e): use option role for prompt menu * test(e2e): update passthrough mention roles * fix: preserve queued references and paginate Azure PRs
181 lines
4.9 KiB
Go
181 lines
4.9 KiB
Go
package models
|
|
|
|
import (
|
|
"github.com/kandev/kandev/internal/agentctl/types/streams"
|
|
"github.com/kandev/kandev/internal/entityrefs"
|
|
)
|
|
|
|
// ShellExecOutputSnapshot is the full bounded output stored on a shell message.
|
|
type ShellExecOutputSnapshot struct {
|
|
ExitCode *int `json:"exit_code,omitempty"`
|
|
Stdout string `json:"stdout,omitempty"`
|
|
Stderr string `json:"stderr,omitempty"`
|
|
Truncated bool `json:"truncated,omitempty"`
|
|
}
|
|
|
|
// ProjectMessageMetadata removes shell output bodies from client message payloads.
|
|
// It copies the normalized shell path so repository-owned metadata is never mutated.
|
|
func ProjectMessageMetadata(metadata map[string]any) map[string]any {
|
|
projected := metadata
|
|
copied := false
|
|
if rawReferences, exists := metadata["entity_references"]; exists {
|
|
projected = copyMetadata(metadata)
|
|
copied = true
|
|
references := entityrefs.NormalizePersisted(rawReferences)
|
|
if len(references) == 0 {
|
|
delete(projected, "entity_references")
|
|
} else {
|
|
projected["entity_references"] = references
|
|
}
|
|
}
|
|
output, ok := shellOutputFromMetadata(metadata)
|
|
if !ok {
|
|
return projected
|
|
}
|
|
normalized, ok := projectNormalizedShell(metadata["normalized"], shellOutputSummary(output))
|
|
if !ok {
|
|
return projected
|
|
}
|
|
if !copied {
|
|
projected = copyMetadata(metadata)
|
|
}
|
|
projected["normalized"] = normalized
|
|
return projected
|
|
}
|
|
|
|
// ExtractShellExecOutput returns the full stored snapshot for normalized shell metadata.
|
|
func ExtractShellExecOutput(metadata map[string]any) (ShellExecOutputSnapshot, bool) {
|
|
return shellOutputFromMetadata(metadata)
|
|
}
|
|
|
|
func shellOutputFromMetadata(metadata map[string]any) (ShellExecOutputSnapshot, bool) {
|
|
if metadata == nil {
|
|
return ShellExecOutputSnapshot{}, false
|
|
}
|
|
raw, ok := metadata["normalized"]
|
|
if !ok || raw == nil {
|
|
return ShellExecOutputSnapshot{}, false
|
|
}
|
|
switch normalized := raw.(type) {
|
|
case *streams.NormalizedPayload:
|
|
if normalized == nil || normalized.ShellExec() == nil {
|
|
return ShellExecOutputSnapshot{}, false
|
|
}
|
|
return shellOutputFromTyped(normalized.ShellExec().Output), true
|
|
case map[string]any:
|
|
shell, shellOK := normalized["shell_exec"].(map[string]any)
|
|
if !shellOK {
|
|
return ShellExecOutputSnapshot{}, false
|
|
}
|
|
return shellOutputFromMap(shell["output"]), true
|
|
default:
|
|
return ShellExecOutputSnapshot{}, false
|
|
}
|
|
}
|
|
|
|
func shellOutputFromTyped(output *streams.ShellExecOutput) ShellExecOutputSnapshot {
|
|
if output == nil {
|
|
return ShellExecOutputSnapshot{}
|
|
}
|
|
return ShellExecOutputSnapshot{
|
|
ExitCode: output.ExitCode,
|
|
Stdout: output.Stdout,
|
|
Stderr: output.Stderr,
|
|
Truncated: output.Truncated,
|
|
}
|
|
}
|
|
|
|
func shellOutputFromMap(raw any) ShellExecOutputSnapshot {
|
|
output, ok := raw.(map[string]any)
|
|
if !ok {
|
|
return ShellExecOutputSnapshot{}
|
|
}
|
|
result := ShellExecOutputSnapshot{
|
|
Stdout: StringFromAny(output["stdout"]),
|
|
Stderr: StringFromAny(output["stderr"]),
|
|
Truncated: boolFromAny(output["truncated"]),
|
|
}
|
|
if exitCode, ok := intFromAny(output["exit_code"]); ok {
|
|
result.ExitCode = &exitCode
|
|
}
|
|
return result
|
|
}
|
|
|
|
func shellOutputSummary(output ShellExecOutputSnapshot) map[string]any {
|
|
summary := map[string]any{
|
|
"has_output": output.Stdout != "" || output.Stderr != "",
|
|
"stdout_bytes": len(output.Stdout),
|
|
"stderr_bytes": len(output.Stderr),
|
|
"truncated": output.Truncated,
|
|
}
|
|
if output.ExitCode != nil {
|
|
summary["exit_code"] = *output.ExitCode
|
|
}
|
|
return summary
|
|
}
|
|
|
|
func projectNormalizedShell(raw any, summary map[string]any) (map[string]any, bool) {
|
|
switch normalized := raw.(type) {
|
|
case *streams.NormalizedPayload:
|
|
if normalized == nil || normalized.ShellExec() == nil {
|
|
return nil, false
|
|
}
|
|
return map[string]any{
|
|
"kind": string(streams.ToolKindShellExec),
|
|
"shell_exec": projectTypedShell(normalized.ShellExec(), summary),
|
|
}, true
|
|
case map[string]any:
|
|
shell, ok := normalized["shell_exec"].(map[string]any)
|
|
if !ok {
|
|
return nil, false
|
|
}
|
|
projectedShell := copyMetadata(shell)
|
|
projectedShell["output"] = summary
|
|
projected := copyMetadata(normalized)
|
|
projected["shell_exec"] = projectedShell
|
|
return projected, true
|
|
default:
|
|
return nil, false
|
|
}
|
|
}
|
|
|
|
func projectTypedShell(shell *streams.ShellExecPayload, summary map[string]any) map[string]any {
|
|
projected := map[string]any{
|
|
"command": shell.Command,
|
|
"output": summary,
|
|
}
|
|
if shell.WorkDir != "" {
|
|
projected["work_dir"] = shell.WorkDir
|
|
}
|
|
if shell.Description != "" {
|
|
projected["description"] = shell.Description
|
|
}
|
|
if shell.Timeout != 0 {
|
|
projected["timeout"] = shell.Timeout
|
|
}
|
|
if shell.Background {
|
|
projected["background"] = true
|
|
}
|
|
return projected
|
|
}
|
|
|
|
func intFromAny(raw any) (int, bool) {
|
|
switch value := raw.(type) {
|
|
case int:
|
|
return value, true
|
|
case int32:
|
|
return int(value), true
|
|
case int64:
|
|
return int(value), true
|
|
case float64:
|
|
return int(value), value == float64(int(value))
|
|
default:
|
|
return 0, false
|
|
}
|
|
}
|
|
|
|
func boolFromAny(raw any) bool {
|
|
value, _ := raw.(bool)
|
|
return value
|
|
}
|