Codex application override added.
This commit is contained in:
1 parent
b887f6ea95
commit
7c53cb39d4
23 files changed
+1771
-155
No files matched your search
@@ -9,6 +9,7 @@ import (
|
||||
"slices"
|
||||
|
||||
"git.stormux.org/storm/fenrir/internal/applicationoverrides"
|
||||
"git.stormux.org/storm/fenrir/internal/applicationoverrides/codex"
|
||||
"git.stormux.org/storm/fenrir/internal/applicationoverrides/networkmanager"
|
||||
"git.stormux.org/storm/fenrir/internal/applicationoverrides/ninjam"
|
||||
"git.stormux.org/storm/fenrir/internal/applicationoverrides/rogue"
|
||||
@@ -189,6 +190,8 @@ func (a *application) select_override(id string) {
|
||||
}
|
||||
var adapter applicationoverrides.Adapter
|
||||
switch id {
|
||||
case "codex":
|
||||
adapter = &codex.Adapter{}
|
||||
case "ninjam":
|
||||
adapter = &ninjam.Adapter{}
|
||||
case "networkmanager":
|
||||
@@ -318,8 +321,8 @@ func (a *application) open_applications() {
|
||||
a.voice.Say(i18n.T("Settings are busy"))
|
||||
return
|
||||
}
|
||||
items := []string{i18n.T("Default"), i18n.T("NetworkManager"), i18n.T("Ninjam"), i18n.T("Rogue")}
|
||||
ids := []string{"default", "networkmanager", "ninjam", "rogue"}
|
||||
items := []string{i18n.T("Default"), i18n.T("NetworkManager"), i18n.T("Ninjam"), i18n.T("Rogue"), i18n.T("Codex")}
|
||||
ids := []string{"default", "networkmanager", "ninjam", "rogue", "codex"}
|
||||
position := 0
|
||||
if selected := a.overrides[a.lastTerminal]; selected != nil {
|
||||
position = slices.Index(ids, selected.id)
|
||||
|
||||
@@ -181,8 +181,9 @@ func TestApplicationsMenuActivatesOnlyOnEnterAndReleasesCapture(t *testing.T) {
|
||||
{"down", "NetworkManager"},
|
||||
{"down", "Ninjam"},
|
||||
{"down", "Rogue"},
|
||||
{"down", "Codex"},
|
||||
{"down", "Default"},
|
||||
{"up", "Rogue"},
|
||||
{"up", "Codex"},
|
||||
} {
|
||||
a.ui.handle(interaction.Key{Name: step.key})
|
||||
if got := voice.messages[len(voice.messages)-1]; got != step.want {
|
||||
@@ -218,6 +219,7 @@ func TestApplicationsMenuActivatesOnlyOnEnterAndReleasesCapture(t *testing.T) {
|
||||
}
|
||||
a.open_applications()
|
||||
a.ui.handle(interaction.Key{Name: "down"}) // Rogue
|
||||
a.ui.handle(interaction.Key{Name: "down"}) // Codex
|
||||
a.ui.handle(interaction.Key{Name: "down"}) // list wraps to Default
|
||||
a.ui.handle(interaction.Key{Name: "enter"})
|
||||
if a.saves_unavailable() || a.engine.Tracking_mode() != "cursor" {
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
// Copyright (c) 2026 Storm Dragon
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
package app
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"git.stormux.org/storm/fenrir/internal/applicationoverrides/codex"
|
||||
)
|
||||
|
||||
func (a *application) codex_event(text string) string {
|
||||
kind, payload, _ := strings.Cut(text, " ")
|
||||
if kind != "CODEX" {
|
||||
return "Unsupported application event"
|
||||
}
|
||||
var event codex.Event
|
||||
if err := json.Unmarshal([]byte(payload), &event); err != nil {
|
||||
return "Invalid Codex event JSON"
|
||||
}
|
||||
if a.options.TerminalMode {
|
||||
if event.Window == "" || event.Window != os.Getenv("WINDOWID") || event.Terminal != 1 {
|
||||
return "Codex event belongs to another terminal"
|
||||
}
|
||||
} else if event.Window != "" || event.Terminal != a.lastTerminal {
|
||||
return "Codex event belongs to another terminal"
|
||||
}
|
||||
selected := a.overrides[event.Terminal]
|
||||
if selected == nil || selected.id != "codex" {
|
||||
return "Codex override is not selected"
|
||||
}
|
||||
if !selected.initialized || a.overrideTransition {
|
||||
a.overrideDirty = true
|
||||
return "Codex override is loading"
|
||||
}
|
||||
adapter, ok := selected.adapter.(*codex.Adapter)
|
||||
if !ok {
|
||||
return "Codex override is unavailable"
|
||||
}
|
||||
if err := adapter.Accept(event); err != nil {
|
||||
return err.Error()
|
||||
}
|
||||
a.overrideDirty = true
|
||||
return ""
|
||||
}
|
||||
@@ -0,0 +1,152 @@
|
||||
// Copyright (c) 2026 Storm Dragon
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
package app
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"os"
|
||||
"slices"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"git.stormux.org/storm/fenrir/internal/applicationoverrides/codex"
|
||||
"git.stormux.org/storm/fenrir/internal/command"
|
||||
"git.stormux.org/storm/fenrir/internal/control"
|
||||
)
|
||||
|
||||
func TestCodexEventRoutingWholeMessagesMuteReviewAndExit(t *testing.T) {
|
||||
t.Setenv("WINDOWID", "42")
|
||||
a, voice := persistence_application(t, true)
|
||||
a.options.TerminalMode = true
|
||||
snapshot := override_screen(1, "OpenAI Codex")
|
||||
override_read(t, a, snapshot)
|
||||
a.select_override("codex")
|
||||
override_read(t, a, snapshot)
|
||||
base := codex.Event{Version: 1, Epoch: 1, Window: "42", Terminal: 1, Session: "fixture-session", Kind: "status", Status: "working"}
|
||||
send := func(event codex.Event, uid int) string {
|
||||
t.Helper()
|
||||
encoded, err := json.Marshal(event)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
reply := make(chan string, 1)
|
||||
a.remote_request(control.Request{UID: uid, Text: "APPLICATION CODEX " + string(encoded), Reply: reply})
|
||||
return <-reply
|
||||
}
|
||||
other := base
|
||||
other.Window = "43"
|
||||
if reply := send(other, os.Geteuid()); !strings.Contains(reply, "another terminal") {
|
||||
t.Fatal(reply)
|
||||
}
|
||||
if reply := send(base, os.Geteuid()+1); !strings.Contains(reply, "not permitted") {
|
||||
t.Fatal(reply)
|
||||
}
|
||||
a.settings.live.Remote.EnableCommandRemote = false
|
||||
if reply := send(base, os.Geteuid()); !strings.Contains(reply, "disabled") {
|
||||
t.Fatal(reply)
|
||||
}
|
||||
a.settings.live.Remote.EnableCommandRemote = true
|
||||
if reply := send(base, os.Geteuid()); reply != "" {
|
||||
t.Fatal(reply)
|
||||
}
|
||||
a.handle_command(command.ReadLiveRegion10)
|
||||
if got := voice.messages[len(voice.messages)-1]; got != "Codex working" {
|
||||
t.Fatal(got)
|
||||
}
|
||||
message := base
|
||||
message.Kind, message.ID, message.Parts, message.Text = "message", "first", 2, "Complete "
|
||||
start := len(voice.messages)
|
||||
if reply := send(message, os.Geteuid()); reply != "" {
|
||||
t.Fatal(reply)
|
||||
}
|
||||
override_read(t, a, snapshot)
|
||||
if len(voice.messages) != start {
|
||||
t.Fatal("partial text spoken", voice.messages[start:])
|
||||
}
|
||||
message.Part, message.Text = 1, "message"
|
||||
if reply := send(message, os.Geteuid()); reply != "" {
|
||||
t.Fatal(reply)
|
||||
}
|
||||
override_read(t, a, snapshot)
|
||||
if got := voice.messages[len(voice.messages)-1]; got != "Complete message" {
|
||||
t.Fatal(got)
|
||||
}
|
||||
message.Part, message.Parts, message.Text = 0, 1, "Complete message"
|
||||
start = len(voice.messages)
|
||||
if reply := send(message, os.Geteuid()); reply != "" {
|
||||
t.Fatal(reply)
|
||||
}
|
||||
override_read(t, a, override_screen(1, "file output and repeated streaming text"))
|
||||
if len(voice.messages) != start {
|
||||
t.Fatal("duplicate or tool output spoken", voice.messages[start:])
|
||||
}
|
||||
a.handle_command(command.Mute)
|
||||
message.ID, message.Text = "muted", "Message while muted"
|
||||
if reply := send(message, os.Geteuid()); reply != "" {
|
||||
t.Fatal(reply)
|
||||
}
|
||||
override_read(t, a, snapshot)
|
||||
if len(voice.messages) != start {
|
||||
t.Fatal("mute bypassed")
|
||||
}
|
||||
a.handle_command(command.Mute)
|
||||
a.handle_command(command.ReadLiveRegion1)
|
||||
if got := voice.messages[len(voice.messages)-1]; got != "Message while muted" {
|
||||
t.Fatal(got)
|
||||
}
|
||||
// Stored replies must not replace raw screen review.
|
||||
a.handle_command(command.CurrentLine)
|
||||
if slices.Contains(voice.messages[len(voice.messages)-1:], "Message while muted") {
|
||||
t.Fatal("history replaced raw review")
|
||||
}
|
||||
base.Status = "closed"
|
||||
if reply := send(base, os.Geteuid()); reply != "" {
|
||||
t.Fatal(reply)
|
||||
}
|
||||
override_read(t, a, override_screen(1, "[Username@host directory] $"))
|
||||
if a.overrideCurrent != nil || a.overrides[1] != nil {
|
||||
t.Fatal("shell did not restore defaults")
|
||||
}
|
||||
}
|
||||
|
||||
func TestCodexWorkingToneUsesExistingSoundGates(t *testing.T) {
|
||||
for _, mode := range []string{"enabled", "sound off", "progress off", "muted", "automatic off", "menu"} {
|
||||
t.Run(mode, func(t *testing.T) {
|
||||
a, _ := persistence_application(t, true)
|
||||
snapshot := override_screen(1, "OpenAI Codex")
|
||||
override_read(t, a, snapshot)
|
||||
a.select_override("codex")
|
||||
override_read(t, a, snapshot)
|
||||
sounds := &fakeSounds{}
|
||||
a.cues = sounds
|
||||
a.settings.live.Presentation.Sound.Enabled = mode != "sound off"
|
||||
a.settings.live.Presentation.Progress.Enabled = mode != "progress off"
|
||||
a.settings.live.Reading.Automatic = mode != "automatic off"
|
||||
a.configure_reader()
|
||||
if mode == "muted" {
|
||||
a.handle_command(command.Mute)
|
||||
}
|
||||
if mode == "menu" {
|
||||
a.open_applications()
|
||||
}
|
||||
adapter := a.overrideCurrent.adapter.(*codex.Adapter)
|
||||
if err := adapter.Accept(codex.Event{Version: 1, Epoch: 1, Session: "fixture-session", Kind: "status", Status: "working"}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
override_read(t, a, snapshot)
|
||||
want := mode == "enabled" || mode == "muted" || mode == "automatic off"
|
||||
if (len(sounds.progress) > 0) != want {
|
||||
t.Fatal(mode, sounds.progress)
|
||||
}
|
||||
if err := adapter.Accept(codex.Event{Version: 1, Epoch: 1, Session: "fixture-session", Kind: "status", Status: "permission"}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
override_read(t, a, snapshot)
|
||||
if _, ok := a.engine.Next_progress(time.Now().Add(2 * time.Second)); ok {
|
||||
t.Fatal("approval wait retained working beep")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -188,6 +188,12 @@ func (a *application) remote_request(request control.Request) {
|
||||
} else {
|
||||
section, text, _ := strings.Cut(request.Text, " ")
|
||||
switch strings.ToUpper(section) {
|
||||
case "APPLICATION":
|
||||
if !a.settings.live.Remote.EnableCommandRemote {
|
||||
response = "Command remote control is disabled"
|
||||
} else {
|
||||
response = a.codex_event(text)
|
||||
}
|
||||
case "COMMAND":
|
||||
if !a.settings.live.Remote.EnableCommandRemote {
|
||||
response = "Command remote control is disabled"
|
||||
@@ -245,7 +251,14 @@ func (a *application) remote_request(request control.Request) {
|
||||
|
||||
func remote_reply(reply chan string, response string) {
|
||||
if response != "" {
|
||||
slog.Warn("remote request rejected", "error", response, "at", time.Now())
|
||||
// An opt-in bridge may be attached while its override is unselected,
|
||||
// loading, or on another VT. Its normal retry must not fill the log.
|
||||
switch response {
|
||||
case "Codex override is not selected", "Codex override is loading", "Codex event belongs to another terminal":
|
||||
slog.Debug("application event deferred", "reason", response, "at", time.Now())
|
||||
default:
|
||||
slog.Warn("remote request rejected", "error", response, "at", time.Now())
|
||||
}
|
||||
}
|
||||
select {
|
||||
case reply <- response:
|
||||
|
||||
Reference in new issue
Block a user