feat(tui,server,config): live config editor + artifact viewer

Two operator features over the existing WebSocket protocol, plus a tui-go nav fix.

Config editor (g / palette "config"):
- core:config gains a thin registry stack: OrchestrationKnobs ([orchestration]
  block), CorrexConfigWriter (round-trip TOML serializer; parseToml(write(c))==c),
  ConfigHolder (AtomicReference live config), and EditableConfig (allowlist of
  editable fields + patch applier; security fields are absent so they can never
  be patched).
- ConfigService validates -> persists (atomic temp+move) -> swaps the holder ->
  rebuilds config-derived services. Live-apply is scoped to safe seams: stage
  timeout, compaction threshold (now a supplier), router knobs (routerFacade
  rebuild), and personalization/project toggles all take effect on the next
  session/turn; in-flight sessions keep the config they started with.
  server.host/port are persisted + badged "restart required", not hot-swapped.
- Protocol: GetConfig / UpdateConfig / ConfigSnapshot; TUI overlay with
  type-aware editing (bool toggle, enum cycle, inline int/string edit) and save.

Artifact viewer (v / palette "artifacts"):
- Server assembles a session's artifacts from the event log and resolves bytes
  from the CAS store (StreamQueries.listArtifacts) — a replay-neutral snapshot
  read. TUI overlay lists artifacts and shows scrollable content.

tui-go fix: workflow failure no longer clears selectedID (which yanked the user
to the list); listNav lands on the first session when nothing is selected
instead of wrapping to a random one.

Config is not event-sourced (it lives in TOML); editing stays outside the log.
This commit is contained in:
2026-06-09 10:18:35 +04:00
parent 89487db72a
commit a92b5a3531
27 changed files with 1629 additions and 72 deletions
+39
View File
@@ -45,6 +45,8 @@ const (
TypeModelList = "model.list"
TypeResourceStatus = "resource.status"
TypeRouterNarration = "router.narration"
TypeArtifactList = "artifact.list"
TypeConfigSnapshot = "config.snapshot"
)
// ServerMessage is a flat decode of every server->client variant. Field names
@@ -107,6 +109,28 @@ type ServerMessage struct {
Stages []StageToolDecl `json:"stages"`
Workflows []WorkflowDto `json:"workflows"`
AssessedIssues []AssessedIssueDto `json:"issues"`
Artifacts []ArtifactDto `json:"artifacts"`
// config.snapshot
ConfigFields []ConfigFieldDto `json:"fields"`
ConfigRestartRequired []string `json:"restartRequired"`
ConfigError *string `json:"error"`
}
type ConfigFieldDto struct {
Key string `json:"key"`
Type string `json:"type"`
Value string `json:"value"`
Options []string `json:"options"`
RestartRequired bool `json:"restartRequired"`
}
type ArtifactDto struct {
ArtifactID string `json:"artifactId"`
StageID string `json:"stageId"`
SchemaVersion int `json:"schemaVersion"`
Phase string `json:"phase"`
Content *string `json:"content"`
}
type SessionStateDto struct {
@@ -228,6 +252,21 @@ func CancelSession(sessionID string) []byte {
return encode("CancelSession", map[string]any{"sessionId": sessionID})
}
// ListArtifacts asks the server for the artifacts produced by a session, with content.
func ListArtifacts(sessionID string) []byte {
return encode("ListArtifacts", map[string]any{"sessionId": sessionID})
}
// GetConfig requests the current editable config (replied to with a config.snapshot).
func GetConfig() []byte {
return encode("GetConfig", map[string]any{})
}
// UpdateConfig asks the server to apply and persist a config patch (key -> string value).
func UpdateConfig(patch map[string]string) []byte {
return encode("UpdateConfig", map[string]any{"patch": patch})
}
// ApprovalResponse answers an approval gate. decision is "APPROVE" or "REJECT".
func ApprovalResponse(requestID, decision string, steeringNote *string) []byte {
return encode("ApprovalResponse", map[string]any{