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:
@@ -167,6 +167,10 @@ func (m Model) handleNormalKey(k tea.KeyMsg) (tea.Model, tea.Cmd) {
|
||||
m.overlayEventIdx = 0
|
||||
case "t":
|
||||
m.overlay = OverlayToolPalette
|
||||
case "v":
|
||||
m.openArtifacts()
|
||||
case "g":
|
||||
m.openConfig()
|
||||
case "m":
|
||||
m.openModelsOverlay()
|
||||
case "w":
|
||||
@@ -340,6 +344,10 @@ func (m Model) autoApprove() (tea.Model, tea.Cmd) {
|
||||
}
|
||||
|
||||
func (m Model) handleOverlayKey(k tea.KeyMsg) (tea.Model, tea.Cmd) {
|
||||
// Config overlay owns all its keys (esc cancels an in-progress edit rather than closing).
|
||||
if m.overlay == OverlayConfig {
|
||||
return m.handleConfigKey(k)
|
||||
}
|
||||
if k.Type == tea.KeyEsc {
|
||||
m.overlay = OverlayNone
|
||||
return m, nil
|
||||
@@ -376,6 +384,29 @@ func (m Model) handleOverlayKey(k tea.KeyMsg) (tea.Model, tea.Cmd) {
|
||||
if runeIs(k, "t") {
|
||||
m.overlay = OverlayNone
|
||||
}
|
||||
case OverlayArtifacts:
|
||||
switch {
|
||||
case k.Type == tea.KeyUp || runeIs(k, "k"):
|
||||
if m.artifactsIndex > 0 {
|
||||
m.artifactsIndex--
|
||||
m.artifactScroll = 0
|
||||
}
|
||||
case k.Type == tea.KeyDown || runeIs(k, "j"):
|
||||
if m.artifactsIndex < len(m.artifacts)-1 {
|
||||
m.artifactsIndex++
|
||||
m.artifactScroll = 0
|
||||
}
|
||||
case k.Type == tea.KeyPgUp:
|
||||
if m.artifactScroll > 0 {
|
||||
m.artifactScroll--
|
||||
}
|
||||
case k.Type == tea.KeyPgDown:
|
||||
if m.artifactScroll < m.artifactContentMaxScroll() {
|
||||
m.artifactScroll++
|
||||
}
|
||||
case runeIs(k, "v"):
|
||||
m.overlay = OverlayNone
|
||||
}
|
||||
case OverlayModels:
|
||||
switch {
|
||||
case k.Type == tea.KeyUp || runeIs(k, "k"):
|
||||
@@ -401,6 +432,23 @@ func (m Model) handleOverlayKey(k tea.KeyMsg) (tea.Model, tea.Cmd) {
|
||||
return m, nil
|
||||
}
|
||||
|
||||
// openArtifacts opens the artifact viewer for the selected session and requests its
|
||||
// artifacts from the server. No-op when no session is selected.
|
||||
func (m *Model) openArtifacts() {
|
||||
if m.selectedID == "" {
|
||||
return
|
||||
}
|
||||
m.overlay = OverlayArtifacts
|
||||
m.artifactsIndex = 0
|
||||
m.artifactScroll = 0
|
||||
// Reuse a cached listing only if it's for this session; otherwise show a loading state.
|
||||
if m.artifactsFor != m.selectedID {
|
||||
m.artifacts = nil
|
||||
m.artifactsLoading = true
|
||||
}
|
||||
m.client.Send(protocol.ListArtifacts(m.selectedID))
|
||||
}
|
||||
|
||||
// openModelsOverlay opens the model picker, pre-selecting the resident model.
|
||||
func (m *Model) openModelsOverlay() {
|
||||
m.overlay = OverlayModels
|
||||
@@ -452,6 +500,8 @@ func paletteCommands() []paletteCmd {
|
||||
{"tools", "tool palette", "tools for the current stage"},
|
||||
{"models", "swap model", "pick / pin the local model"},
|
||||
{"events", "event inspector", "browse the event stream"},
|
||||
{"artifacts", "view artifacts", "browse this session's artifacts"},
|
||||
{"config", "edit config", "view / change correx settings"},
|
||||
{"mode", "toggle mode", "switch chat / steering"},
|
||||
{"cancel", "cancel session", "stop the selected session"},
|
||||
{"back", "back to list", "leave the current session"},
|
||||
@@ -486,6 +536,10 @@ func (m Model) execPalette(id string) (tea.Model, tea.Cmd) {
|
||||
case "events":
|
||||
m.overlay = OverlayEventInspector
|
||||
m.overlayEventIdx = 0
|
||||
case "artifacts":
|
||||
m.openArtifacts()
|
||||
case "config":
|
||||
m.openConfig()
|
||||
case "mode":
|
||||
m.cycleChatMode()
|
||||
case "cancel":
|
||||
@@ -683,6 +737,13 @@ func (m *Model) listNav(dir int) {
|
||||
break
|
||||
}
|
||||
}
|
||||
// No current selection (e.g. nothing entered yet): start at the first
|
||||
// session rather than wrapping off the end into an arbitrary entry.
|
||||
if idx < 0 {
|
||||
m.selectedID = list[0].ID
|
||||
m.wfIndex = -1
|
||||
return
|
||||
}
|
||||
n := idx + dir
|
||||
if n < 0 {
|
||||
n = len(list) - 1
|
||||
|
||||
Reference in New Issue
Block a user