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
+24 -4
View File
@@ -137,9 +137,6 @@ func (m *Model) applyServer(msg protocol.ServerMessage) {
if s := m.session(msg.SessionID); s != nil {
s.Active = false
}
if msg.SessionID == m.selectedID {
m.selectedID = ""
}
case protocol.TypeStageStarted:
if s := m.session(msg.SessionID); s != nil {
s.CurrentStage = msg.StageID
@@ -277,6 +274,28 @@ func (m *Model) applyServer(msg protocol.ServerMessage) {
m.gpuTotalMB = msg.GpuMemoryTotalMb
m.gpuUtil = msg.GpuUtilizationPct
m.ramMB = msg.ProcessRssMb
case protocol.TypeArtifactList:
m.artifacts = msg.Artifacts
m.artifactsFor = msg.SessionID
m.artifactsLoading = false
m.artifactScroll = 0
if m.artifactsIndex >= len(m.artifacts) {
m.artifactsIndex = 0
}
case protocol.TypeConfigSnapshot:
m.configFields = msg.ConfigFields
m.configRestart = msg.ConfigRestartRequired
m.configLoading = false
if msg.ConfigError != nil {
m.configError = *msg.ConfigError
} else {
// A clean snapshot means the staged edits were accepted (or this is a fresh fetch).
m.configError = ""
m.configStaged = map[string]string{}
}
if m.configIndex >= len(m.configFields) {
m.configIndex = 0
}
}
// Background-update badge for non-selected sessions.
@@ -290,7 +309,8 @@ func sessionIDOf(msg protocol.ServerMessage) string {
case protocol.TypeStageManifest, protocol.TypeSnapshotComplete,
protocol.TypeProtocolError, protocol.TypeProviderStatus,
protocol.TypeWorkflowList, protocol.TypeRouterResponse,
protocol.TypeModelChanged, protocol.TypeModelList, protocol.TypeResourceStatus:
protocol.TypeModelChanged, protocol.TypeModelList, protocol.TypeResourceStatus,
protocol.TypeArtifactList, protocol.TypeConfigSnapshot:
return ""
default:
return msg.SessionID