proc events, bypass shortcuts, Kate session fix
Backend: - Add proc.start/proc.exit events for background process lifecycle - Add proc idx ctl command for machine-readable process listing (TSV) - Add event.pub file for external event publishing - Add ListProcsIdx to toolclient and toolsrv - Fix bypass commands to use Setpgid for process group isolation GUI: - Add configurable bypass approval shortcuts (Ctrl+Y/Ctrl+N default) - Add Keyboard Shortcuts section to Settings dialog - Store shortcuts in theme.conf Kate: - Fix 'Start Session Here' - use rdwr for session/new endpoint
This commit is contained in:
parent
b9026bb48b
commit
1c02b50add
|
|
@ -64,6 +64,7 @@ type Agent struct {
|
||||||
save func() // trigger debounced persistence
|
save func() // trigger debounced persistence
|
||||||
flush func() // immediately flush persistence
|
flush func() // immediately flush persistence
|
||||||
onStateChange func(agentID, state string) // optional callback for state changes
|
onStateChange func(agentID, state string) // optional callback for state changes
|
||||||
|
onProcStart func(agentID string, pid int, tool, cmd string) // optional callback for proc start
|
||||||
|
|
||||||
// Chat log — the rendered output stream, exposed via 9P.
|
// Chat log — the rendered output stream, exposed via 9P.
|
||||||
chatMu sync.RWMutex
|
chatMu sync.RWMutex
|
||||||
|
|
|
||||||
|
|
@ -252,6 +252,10 @@ func (ag *Agent) execOne(ctx context.Context, call backend.ToolCall) (toolResult
|
||||||
isErr = true
|
isErr = true
|
||||||
} else {
|
} else {
|
||||||
result = fmt.Sprintf("<system-proc-background>\nid=%d cmd=%q\n</system-proc-background>", pid, cmdDesc)
|
result = fmt.Sprintf("<system-proc-background>\nid=%d cmd=%q\n</system-proc-background>", pid, cmdDesc)
|
||||||
|
// Notify proc.start via callback
|
||||||
|
if ag.onProcStart != nil {
|
||||||
|
ag.onProcStart(ag.id, pid, call.Name, cmdDesc)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
ag.emit(Event{Role: "tool", Name: call.Name, Content: result, OutputFormat: toolOutputFormat(rt, call.Name)})
|
ag.emit(Event{Role: "tool", Name: call.Name, Content: result, OutputFormat: toolOutputFormat(rt, call.Name)})
|
||||||
|
|
|
||||||
|
|
@ -82,6 +82,11 @@ func (ag *Agent) SetOnStateChange(fn func(agentID, state string)) {
|
||||||
ag.onStateChange = fn
|
ag.onStateChange = fn
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// SetOnProcStart sets a callback invoked when a background process starts.
|
||||||
|
func (ag *Agent) SetOnProcStart(fn func(agentID string, pid int, tool, cmd string)) {
|
||||||
|
ag.onProcStart = fn
|
||||||
|
}
|
||||||
|
|
||||||
// setReply sets the agent's last response.
|
// setReply sets the agent's last response.
|
||||||
func (ag *Agent) setReply(reply string) {
|
func (ag *Agent) setReply(reply string) {
|
||||||
ag.stateMu.Lock()
|
ag.stateMu.Lock()
|
||||||
|
|
|
||||||
|
|
@ -245,6 +245,18 @@ func buildTreeSpec(cfg *Config) virtfs.FsNodeDecl {
|
||||||
return nil, base, nil
|
return nil, base, nil
|
||||||
}),
|
}),
|
||||||
),
|
),
|
||||||
|
virtfs.FileNode("event.pub", 0220,
|
||||||
|
virtfs.Doc("Publish event: write topic<tab>payload"),
|
||||||
|
virtfs.Write(func(data []byte) error {
|
||||||
|
line := strings.TrimSpace(string(data))
|
||||||
|
topic, payload, _ := strings.Cut(line, "\t")
|
||||||
|
if topic == "" {
|
||||||
|
return fmt.Errorf("empty topic")
|
||||||
|
}
|
||||||
|
session.PublishEvent(topic, payload)
|
||||||
|
return nil
|
||||||
|
}),
|
||||||
|
),
|
||||||
virtfs.FileNode("generate", 0666,
|
virtfs.FileNode("generate", 0666,
|
||||||
virtfs.Doc("One-shot LLM generation"),
|
virtfs.Doc("One-shot LLM generation"),
|
||||||
virtfs.Rdwr(func(_ context.Context, data []byte) ([]byte, error) {
|
virtfs.Rdwr(func(_ context.Context, data []byte) ([]byte, error) {
|
||||||
|
|
@ -1105,7 +1117,7 @@ func buildAgentChildren(a *agent.Agent, s *session.Session) []virtfs.FsNodeDecl
|
||||||
{"cwd", "print the working directory", func(args []string) ([]byte, error) {
|
{"cwd", "print the working directory", func(args []string) ([]byte, error) {
|
||||||
return []byte(a.Cwd() + "\n"), nil
|
return []byte(a.Cwd() + "\n"), nil
|
||||||
}},
|
}},
|
||||||
{"proc", "manage background procs: proc [top|term|kill|out|dismiss <pid>]", func(args []string) ([]byte, error) {
|
{"proc", "manage background procs: proc [top|idx|term|kill|out|dismiss <pid>]", func(args []string) ([]byte, error) {
|
||||||
ts := a.ToolServer()
|
ts := a.ToolServer()
|
||||||
if ts == nil {
|
if ts == nil {
|
||||||
return nil, fmt.Errorf("no tool server")
|
return nil, fmt.Errorf("no tool server")
|
||||||
|
|
@ -1122,6 +1134,12 @@ func buildAgentChildren(a *agent.Agent, s *session.Session) []virtfs.FsNodeDecl
|
||||||
}
|
}
|
||||||
subcmd := args[0]
|
subcmd := args[0]
|
||||||
switch subcmd {
|
switch subcmd {
|
||||||
|
case "idx":
|
||||||
|
data, err := ts.ListProcsIdx()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return data, nil
|
||||||
case "term":
|
case "term":
|
||||||
if len(args) < 2 {
|
if len(args) < 2 {
|
||||||
return nil, fmt.Errorf("proc term requires pid")
|
return nil, fmt.Errorf("proc term requires pid")
|
||||||
|
|
@ -1172,7 +1190,7 @@ func buildAgentChildren(a *agent.Agent, s *session.Session) []virtfs.FsNodeDecl
|
||||||
}
|
}
|
||||||
return []byte("ok\n"), nil
|
return []byte("ok\n"), nil
|
||||||
default:
|
default:
|
||||||
return nil, fmt.Errorf("unknown proc subcommand: %s (use: top, term <pid>, kill <pid>, out <pid>, dismiss <pid>)", subcmd)
|
return nil, fmt.Errorf("unknown proc subcommand: %s (use: top, idx, term <pid>, kill <pid>, out <pid>, dismiss <pid>)", subcmd)
|
||||||
}
|
}
|
||||||
}},
|
}},
|
||||||
{"systemprompt", "print the rendered system prompt", func(_ []string) ([]byte, error) {
|
{"systemprompt", "print the rendered system prompt", func(_ []string) ([]byte, error) {
|
||||||
|
|
|
||||||
|
|
@ -21,6 +21,11 @@ func wireAgentEvents(sessID string, ag *agent.Agent) {
|
||||||
topic := "session." + sessID + ".agent." + agentID + ".state"
|
topic := "session." + sessID + ".agent." + agentID + ".state"
|
||||||
session.PublishEvent(topic, state)
|
session.PublishEvent(topic, state)
|
||||||
})
|
})
|
||||||
|
ag.SetOnProcStart(func(agentID string, pid int, tool, cmd string) {
|
||||||
|
topic := "session." + sessID + ".agent." + agentID + ".proc.start"
|
||||||
|
payload := fmt.Sprintf("%d\t%s\t%s", pid, tool, cmd)
|
||||||
|
session.PublishEvent(topic, payload)
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// ctlCmd is one control verb: its name, a one-line description for the
|
// ctlCmd is one control verb: its name, a one-line description for the
|
||||||
|
|
|
||||||
|
|
@ -408,6 +408,23 @@ func (c *ToolsrvConn) ListProcs() ([]byte, error) {
|
||||||
return io.ReadAll(fid)
|
return io.ReadAll(fid)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ListProcsIdx returns a machine-readable TSV proc listing.
|
||||||
|
// Format: pid<TAB>status<TAB>exit_code<TAB>tool<TAB>cmd<TAB>start_unix<TAB>runtime_sec
|
||||||
|
func (c *ToolsrvConn) ListProcsIdx() ([]byte, error) {
|
||||||
|
fid, err := c.fsys.Open("proc/idx", plan9.ORDWR)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer fid.Close()
|
||||||
|
if _, err := fid.Write([]byte(c.agentID)); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if _, err := fid.Seek(0, 0); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return io.ReadAll(fid)
|
||||||
|
}
|
||||||
|
|
||||||
// readProcInfo reads info about a single proc.
|
// readProcInfo reads info about a single proc.
|
||||||
func (c *ToolsrvConn) readProcInfo(name string) map[string]any {
|
func (c *ToolsrvConn) readProcInfo(name string) map[string]any {
|
||||||
path := fmt.Sprintf("proc/%s/stat", name)
|
path := fmt.Sprintf("proc/%s/stat", name)
|
||||||
|
|
|
||||||
|
|
@ -293,6 +293,16 @@ func executeDirectUnsandboxed(ctx context.Context, cmd, cwd string, env map[stri
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Set process group so we can signal all children
|
||||||
|
execCmd.SysProcAttr = &syscall.SysProcAttr{Setpgid: true, Pdeathsig: syscall.SIGKILL}
|
||||||
|
execCmd.Cancel = func() error {
|
||||||
|
if execCmd.Process != nil {
|
||||||
|
syscall.Kill(-execCmd.Process.Pid, syscall.SIGTERM)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
execCmd.WaitDelay = 5 * time.Second
|
||||||
|
|
||||||
var stdout, stderr bytes.Buffer
|
var stdout, stderr bytes.Buffer
|
||||||
if streamOut != nil {
|
if streamOut != nil {
|
||||||
// Streaming mode: tee to both the stream and local buffer
|
// Streaming mode: tee to both the stream and local buffer
|
||||||
|
|
|
||||||
|
|
@ -518,6 +518,40 @@ func (st *State) ListProcsForAgent(agentID string) string {
|
||||||
return buf.String()
|
return buf.String()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ListProcsIdxForAgent returns a machine-readable TSV list of processes.
|
||||||
|
// If agentID is empty, returns all processes.
|
||||||
|
// Format: pid<TAB>status<TAB>exit_code<TAB>tool<TAB>cmd<TAB>start_unix<TAB>runtime_sec
|
||||||
|
func (st *State) ListProcsIdxForAgent(agentID string) string {
|
||||||
|
st.procMu.Lock()
|
||||||
|
defer st.procMu.Unlock()
|
||||||
|
var buf bytes.Buffer
|
||||||
|
for pid, p := range st.procs {
|
||||||
|
p.mu.Lock()
|
||||||
|
if agentID != "" && p.AgentID != agentID {
|
||||||
|
p.mu.Unlock()
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
status := "running"
|
||||||
|
exitCode := -1
|
||||||
|
if p.Exited {
|
||||||
|
status = "exited"
|
||||||
|
exitCode = p.ExitCode
|
||||||
|
}
|
||||||
|
startUnix := p.StartTime.Unix()
|
||||||
|
var runtime float64
|
||||||
|
if p.Exited && !p.EndTime.IsZero() {
|
||||||
|
runtime = p.EndTime.Sub(p.StartTime).Seconds()
|
||||||
|
} else {
|
||||||
|
runtime = time.Since(p.StartTime).Seconds()
|
||||||
|
}
|
||||||
|
tool := p.Tool
|
||||||
|
cmd := p.Cmd
|
||||||
|
p.mu.Unlock()
|
||||||
|
fmt.Fprintf(&buf, "%d\t%s\t%d\t%s\t%s\t%d\t%.1f\n", pid, status, exitCode, tool, cmd, startUnix, runtime)
|
||||||
|
}
|
||||||
|
return buf.String()
|
||||||
|
}
|
||||||
|
|
||||||
// KillAll sends SIGKILL to all running procs and cancels their contexts.
|
// KillAll sends SIGKILL to all running procs and cancels their contexts.
|
||||||
// Called on shutdown to ensure no orphaned processes.
|
// Called on shutdown to ensure no orphaned processes.
|
||||||
func (st *State) KillAll() {
|
func (st *State) KillAll() {
|
||||||
|
|
|
||||||
|
|
@ -195,6 +195,13 @@ func Spec(srv *Server) virtfs.FsNodeDecl {
|
||||||
return []byte(srv.Fs.ListProcsForAgent(agentID)), nil
|
return []byte(srv.Fs.ListProcsForAgent(agentID)), nil
|
||||||
}),
|
}),
|
||||||
),
|
),
|
||||||
|
virtfs.FileNode("idx", 0666,
|
||||||
|
virtfs.Doc("Machine-readable proc index: write agent ID, read TSV (pid status exit_code tool cmd start_unix runtime_sec)"),
|
||||||
|
virtfs.Rdwr(func(_ context.Context, data []byte) ([]byte, error) {
|
||||||
|
agentID := strings.TrimSpace(string(data))
|
||||||
|
return []byte(srv.Fs.ListProcsIdxForAgent(agentID)), nil
|
||||||
|
}),
|
||||||
|
),
|
||||||
virtfs.FileNode("new", 0666,
|
virtfs.FileNode("new", 0666,
|
||||||
virtfs.Doc("Execute tool: write token + tool + args, read result (blocking)"),
|
virtfs.Doc("Execute tool: write token + tool + args, read result (blocking)"),
|
||||||
virtfs.Rdwr(func(ctx context.Context, data []byte) ([]byte, error) {
|
virtfs.Rdwr(func(ctx context.Context, data []byte) ([]byte, error) {
|
||||||
|
|
|
||||||
|
|
@ -241,13 +241,15 @@ monitor:
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// pushProcInterrupt pushes the completed proc output to the agent's prompt.
|
// pushProcInterrupt pushes the completed proc output to the agent's prompt
|
||||||
|
// and publishes a proc.exit event.
|
||||||
func pushProcInterrupt(ctx context.Context, proc *server.Proc) {
|
func pushProcInterrupt(ctx context.Context, proc *server.Proc) {
|
||||||
// Build the interrupt message
|
// Build the interrupt message
|
||||||
proc.Lock()
|
proc.Lock()
|
||||||
output := proc.OutputString()
|
output := proc.OutputString()
|
||||||
exitCode := proc.ExitCode
|
exitCode := proc.ExitCode
|
||||||
cmd := proc.Cmd
|
cmd := proc.Cmd
|
||||||
|
tool := proc.Tool
|
||||||
pid := proc.ID
|
pid := proc.ID
|
||||||
sid := proc.SessionID
|
sid := proc.SessionID
|
||||||
aid := proc.AgentID
|
aid := proc.AgentID
|
||||||
|
|
@ -290,6 +292,15 @@ func pushProcInterrupt(ctx context.Context, proc *server.Proc) {
|
||||||
}
|
}
|
||||||
defer fsys.Close()
|
defer fsys.Close()
|
||||||
|
|
||||||
|
// Publish proc.exit event
|
||||||
|
eventPath := "event.pub"
|
||||||
|
eventTopic := fmt.Sprintf("session.%s.agent.%s.proc.exit", sid, aid)
|
||||||
|
eventPayload := fmt.Sprintf("%d\t%d\t%s\t%s", pid, exitCode, tool, cmd)
|
||||||
|
if evFid, err := fsys.Open(eventPath, plan9.OWRITE); err == nil {
|
||||||
|
evFid.Write([]byte(eventTopic + "\t" + eventPayload))
|
||||||
|
evFid.Close()
|
||||||
|
}
|
||||||
|
|
||||||
// Write to session/{sid}/agent/{aid}/prompt
|
// Write to session/{sid}/agent/{aid}/prompt
|
||||||
promptPath := fmt.Sprintf("session/%s/agent/%s/prompt", sid, aid)
|
promptPath := fmt.Sprintf("session/%s/agent/%s/prompt", sid, aid)
|
||||||
fid, err := fsys.Open(promptPath, plan9.OWRITE)
|
fid, err := fsys.Open(promptPath, plan9.OWRITE)
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,144 @@
|
||||||
|
# Proc Window Feature Design
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
A dedicated window for managing background processes (tools running in background mode). Accessible via context menu on sessions/agents in the tree view.
|
||||||
|
|
||||||
|
## User Flow
|
||||||
|
|
||||||
|
1. Right-click session/agent → "Processes..."
|
||||||
|
2. Opens ProcWindow showing all background procs for that scope
|
||||||
|
3. Window tracks the active session (when main GUI switches session, proc window updates)
|
||||||
|
4. Can also be "pinned" to a specific session/agent
|
||||||
|
|
||||||
|
## UI Components
|
||||||
|
|
||||||
|
### ProcWindow.qml
|
||||||
|
|
||||||
|
```
|
||||||
|
┌─────────────────────────────────────────────────────────────┐
|
||||||
|
│ Processes - {session}/{agent} [📌] [×] │
|
||||||
|
├─────────────────────────────────────────────────────────────┤
|
||||||
|
│ ┌─────────────────────────────────────────────────────────┐ │
|
||||||
|
│ │ PID │ Tool │ Status │ Runtime │ Actions │ │
|
||||||
|
│ ├─────┼───────────┼─────────┼──────────┼─────────────────┤ │
|
||||||
|
│ │ 5 │ shell │ running │ 2m 34s │ [▶] [■] [×] │ │
|
||||||
|
│ │ 3 │ web_fetch │ exited │ 45s │ [▶] [🗑] │ │
|
||||||
|
│ │ 1 │ shell │ exited │ 12s │ [▶] [🗑] │ │
|
||||||
|
│ └─────────────────────────────────────────────────────────┘ │
|
||||||
|
├─────────────────────────────────────────────────────────────┤
|
||||||
|
│ Output for PID 5 [Clear] │
|
||||||
|
├─────────────────────────────────────────────────────────────┤
|
||||||
|
│ $ npm install │
|
||||||
|
│ npm WARN deprecated... │
|
||||||
|
│ added 123 packages in 45s │
|
||||||
|
│ █ │
|
||||||
|
└─────────────────────────────────────────────────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
### Actions
|
||||||
|
|
||||||
|
| Icon | Action | Description |
|
||||||
|
|------|--------|-------------|
|
||||||
|
| ▶ | Stream | Open output stream (updates in real-time) |
|
||||||
|
| ■ | Term | Send SIGTERM (graceful stop) |
|
||||||
|
| × | Kill | Send SIGKILL (force kill) |
|
||||||
|
| 🗑 | Dismiss | Remove from list (exited procs only) |
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
1. **Live refresh**: Subscribe to proc events or poll `proc idx`
|
||||||
|
2. **Output streaming**: Read proc output via `proc out <pid>`
|
||||||
|
3. **Session tracking**: Follow active session in main GUI (optional pin)
|
||||||
|
4. **Multi-agent view**: When opened on session, show all agents' procs
|
||||||
|
5. **Auto-scroll**: Output pane auto-scrolls unless user scrolls up
|
||||||
|
|
||||||
|
## Backend Implementation Status
|
||||||
|
|
||||||
|
### ✅ Phase 1: Backend (DONE)
|
||||||
|
|
||||||
|
#### Proc Events
|
||||||
|
- `session.{sid}.agent.{aid}.proc.start` — emitted when background proc spawns
|
||||||
|
- Payload: `{pid}\t{tool}\t{cmd}`
|
||||||
|
- `session.{sid}.agent.{aid}.proc.exit` — emitted when background proc exits
|
||||||
|
- Payload: `{pid}\t{exit_code}\t{tool}\t{cmd}`
|
||||||
|
|
||||||
|
#### Machine-Readable Proc Index
|
||||||
|
- `proc idx` ctl command returns TSV format:
|
||||||
|
- `pid<TAB>status<TAB>exit_code<TAB>tool<TAB>cmd<TAB>start_unix<TAB>runtime_sec`
|
||||||
|
- Status is `running` or `exited`
|
||||||
|
- Exit code is `-1` for running processes
|
||||||
|
|
||||||
|
#### Event Publishing Endpoint
|
||||||
|
- `event.pub` file at root namespace accepts writes: `topic<TAB>payload`
|
||||||
|
- Used by toolsrv to publish proc.exit events back to olliesrv
|
||||||
|
|
||||||
|
### 🔲 Phase 2: GUI (TODO)
|
||||||
|
|
||||||
|
1. Create ProcModel (C++ model for proc list)
|
||||||
|
2. Create ProcWindow.qml
|
||||||
|
3. Add context menu action to SessionTree.qml
|
||||||
|
4. Wire up event subscription for live updates
|
||||||
|
|
||||||
|
### 🔲 Phase 3: Polish (TODO)
|
||||||
|
|
||||||
|
1. Session tracking / pin toggle
|
||||||
|
2. Output search/filter
|
||||||
|
3. Keyboard shortcuts
|
||||||
|
4. Copy output to clipboard
|
||||||
|
|
||||||
|
## Files Modified (Phase 1)
|
||||||
|
|
||||||
|
- `cmd/olliesrv/internal/agent/agent.go` — added `onProcStart` callback
|
||||||
|
- `cmd/olliesrv/internal/agent/state.go` — added `SetOnProcStart`
|
||||||
|
- `cmd/olliesrv/internal/agent/dispatch.go` — call callback on proc start
|
||||||
|
- `cmd/olliesrv/internal/fs/support.go` — wire callback to publish event
|
||||||
|
- `cmd/olliesrv/internal/fs/spec.go` — added `event.pub` file, `proc idx` command
|
||||||
|
- `cmd/olliesrv/internal/toolclient/toolsrv.go` — added `ListProcsIdx`
|
||||||
|
- `cmd/toolsrv/internal/server/proc.go` — added `ListProcsIdxForAgent`
|
||||||
|
- `cmd/toolsrv/internal/server/server.go` — added `proc/idx` file
|
||||||
|
- `cmd/toolsrv/main.go` — publish proc.exit event via event.pub
|
||||||
|
- `doc/event-topics.md` — documented new proc events
|
||||||
|
|
||||||
|
## API Reference
|
||||||
|
|
||||||
|
### ctl Commands
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Human-readable proc list
|
||||||
|
echo "proc top" | ollie-9p rdwr session/$SID/agent/$AID/ctl
|
||||||
|
|
||||||
|
# Machine-readable proc index (TSV)
|
||||||
|
echo "proc idx" | ollie-9p rdwr session/$SID/agent/$AID/ctl
|
||||||
|
|
||||||
|
# Get proc output
|
||||||
|
echo "proc out 5" | ollie-9p rdwr session/$SID/agent/$AID/ctl
|
||||||
|
|
||||||
|
# Signal proc
|
||||||
|
echo "proc term 5" | ollie-9p rdwr session/$SID/agent/$AID/ctl
|
||||||
|
echo "proc kill 5" | ollie-9p rdwr session/$SID/agent/$AID/ctl
|
||||||
|
|
||||||
|
# Dismiss exited proc
|
||||||
|
echo "proc dismiss 5" | ollie-9p rdwr session/$SID/agent/$AID/ctl
|
||||||
|
```
|
||||||
|
|
||||||
|
### Event Subscription
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# All proc events for all sessions/agents
|
||||||
|
echo "session.*.agent.*.proc.>" | ollie-9p rdwrs event
|
||||||
|
|
||||||
|
# Proc events for specific session
|
||||||
|
echo "session.abc123.agent.*.proc.>" | ollie-9p rdwrs event
|
||||||
|
|
||||||
|
# Only proc.start events
|
||||||
|
echo "session.*.agent.*.proc.start" | ollie-9p rdwrs event
|
||||||
|
```
|
||||||
|
|
||||||
|
### proc idx Output Format
|
||||||
|
|
||||||
|
```
|
||||||
|
pid status exit_code tool cmd start_unix runtime_sec
|
||||||
|
5 running -1 shell make test 1723456789 45.2
|
||||||
|
3 exited 0 web_fetch fetch docs 1723456700 12.5
|
||||||
|
```
|
||||||
|
|
@ -29,6 +29,13 @@ session.{sessionId}.agent.{agentId}.{type}
|
||||||
| `session.{sid}.agent.{aid}.bypass.request` | `{id}\t{cmd}\t{cwd}` | Bypass approval requested |
|
| `session.{sid}.agent.{aid}.bypass.request` | `{id}\t{cmd}\t{cwd}` | Bypass approval requested |
|
||||||
| `session.{sid}.agent.{aid}.bypass.resolved` | `{id}\t{action}` | Bypass resolved (approved/denied) |
|
| `session.{sid}.agent.{aid}.bypass.resolved` | `{id}\t{action}` | Bypass resolved (approved/denied) |
|
||||||
|
|
||||||
|
## Process Events
|
||||||
|
|
||||||
|
| Topic | Payload | Description |
|
||||||
|
|-------|---------|-------------|
|
||||||
|
| `session.{sid}.agent.{aid}.proc.start` | `{pid}\t{tool}\t{cmd}` | Background process started |
|
||||||
|
| `session.{sid}.agent.{aid}.proc.exit` | `{pid}\t{exit_code}\t{tool}\t{cmd}` | Background process exited |
|
||||||
|
|
||||||
## Filter Patterns
|
## Filter Patterns
|
||||||
|
|
||||||
Subscribe to specific events using the streaming rdwr pattern:
|
Subscribe to specific events using the streaming rdwr pattern:
|
||||||
|
|
|
||||||
|
|
@ -620,6 +620,22 @@ Connections {
|
||||||
chatPane.hasPendingBypass = false
|
chatPane.hasPendingBypass = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Public functions for keyboard shortcuts
|
||||||
|
function approveBypass() {
|
||||||
|
if (chatPane.pendingBypass) {
|
||||||
|
ollie.resolveBypass(chatPane.pendingBypass.sessionId, chatPane.pendingBypass.agentId, chatPane.pendingBypass.id, true)
|
||||||
|
chatPane.pendingBypass = null
|
||||||
|
chatPane.hasPendingBypass = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
function denyBypass() {
|
||||||
|
if (chatPane.pendingBypass) {
|
||||||
|
ollie.resolveBypass(chatPane.pendingBypass.sessionId, chatPane.pendingBypass.agentId, chatPane.pendingBypass.id, false)
|
||||||
|
chatPane.pendingBypass = null
|
||||||
|
chatPane.hasPendingBypass = false
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,7 @@ import QtQuick.Layouts 1.15
|
||||||
Dialog {
|
Dialog {
|
||||||
id: root
|
id: root
|
||||||
title: "Settings"
|
title: "Settings"
|
||||||
width: 380
|
width: 420
|
||||||
modal: false
|
modal: false
|
||||||
x: (parent ? (parent.width - width) / 2 : 0)
|
x: (parent ? (parent.width - width) / 2 : 0)
|
||||||
y: (parent ? (parent.height - height) / 2 : 0)
|
y: (parent ? (parent.height - height) / 2 : 0)
|
||||||
|
|
@ -117,6 +117,51 @@ Dialog {
|
||||||
Layout.topMargin: 8
|
Layout.topMargin: 8
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Label {
|
||||||
|
text: "Keyboard Shortcuts"
|
||||||
|
font.bold: true
|
||||||
|
}
|
||||||
|
|
||||||
|
GridLayout {
|
||||||
|
columns: 2
|
||||||
|
columnSpacing: 12
|
||||||
|
rowSpacing: 8
|
||||||
|
Layout.fillWidth: true
|
||||||
|
|
||||||
|
Label { text: "Approve bypass:" }
|
||||||
|
TextField {
|
||||||
|
id: approveKeyField
|
||||||
|
Layout.fillWidth: true
|
||||||
|
text: theme.approveBypassKey
|
||||||
|
placeholderText: "e.g. Ctrl+Y"
|
||||||
|
onEditingFinished: theme.approveBypassKey = text
|
||||||
|
}
|
||||||
|
|
||||||
|
Label { text: "Deny bypass:" }
|
||||||
|
TextField {
|
||||||
|
id: denyKeyField
|
||||||
|
Layout.fillWidth: true
|
||||||
|
text: theme.denyBypassKey
|
||||||
|
placeholderText: "e.g. Ctrl+N"
|
||||||
|
onEditingFinished: theme.denyBypassKey = text
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Label {
|
||||||
|
text: "Use Qt key sequence format (e.g. Ctrl+Y, Alt+A, F5)"
|
||||||
|
opacity: 0.5
|
||||||
|
font.pixelSize: Qt.application.font.pixelSize * 0.85
|
||||||
|
wrapMode: Text.Wrap
|
||||||
|
Layout.fillWidth: true
|
||||||
|
}
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
Layout.fillWidth: true
|
||||||
|
height: 1
|
||||||
|
color: palette.mid
|
||||||
|
Layout.topMargin: 8
|
||||||
|
}
|
||||||
|
|
||||||
Label {
|
Label {
|
||||||
text: "Font"
|
text: "Font"
|
||||||
font.bold: true
|
font.bold: true
|
||||||
|
|
|
||||||
|
|
@ -78,6 +78,19 @@ ApplicationWindow {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Bypass approval/denial shortcuts (configurable in Settings)
|
||||||
|
Shortcut {
|
||||||
|
sequence: theme.approveBypassKey
|
||||||
|
enabled: theme.approveBypassKey !== "" && chatPane.hasPendingBypass
|
||||||
|
onActivated: chatPane.approveBypass()
|
||||||
|
}
|
||||||
|
|
||||||
|
Shortcut {
|
||||||
|
sequence: theme.denyBypassKey
|
||||||
|
enabled: theme.denyBypassKey !== "" && chatPane.hasPendingBypass
|
||||||
|
onActivated: chatPane.denyBypass()
|
||||||
|
}
|
||||||
|
|
||||||
property bool sidebarVisible: true
|
property bool sidebarVisible: true
|
||||||
property bool quitting: false
|
property bool quitting: false
|
||||||
|
|
||||||
|
|
@ -210,6 +223,7 @@ ApplicationWindow {
|
||||||
}
|
}
|
||||||
|
|
||||||
ChatPane {
|
ChatPane {
|
||||||
|
id: chatPane
|
||||||
SplitView.fillWidth: true
|
SplitView.fillWidth: true
|
||||||
SplitView.fillHeight: true
|
SplitView.fillHeight: true
|
||||||
sidebarVisible: root.sidebarVisible
|
sidebarVisible: root.sidebarVisible
|
||||||
|
|
|
||||||
|
|
@ -394,6 +394,34 @@ void ThemeManager::setCloseToTray(bool enabled)
|
||||||
emit closeToTrayChanged();
|
emit closeToTrayChanged();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
QString ThemeManager::approveBypassKey() const
|
||||||
|
{
|
||||||
|
return m_approveBypassKey;
|
||||||
|
}
|
||||||
|
|
||||||
|
void ThemeManager::setApproveBypassKey(const QString &key)
|
||||||
|
{
|
||||||
|
if (key == m_approveBypassKey)
|
||||||
|
return;
|
||||||
|
m_approveBypassKey = key;
|
||||||
|
saveSettings();
|
||||||
|
emit shortcutsChanged();
|
||||||
|
}
|
||||||
|
|
||||||
|
QString ThemeManager::denyBypassKey() const
|
||||||
|
{
|
||||||
|
return m_denyBypassKey;
|
||||||
|
}
|
||||||
|
|
||||||
|
void ThemeManager::setDenyBypassKey(const QString &key)
|
||||||
|
{
|
||||||
|
if (key == m_denyBypassKey)
|
||||||
|
return;
|
||||||
|
m_denyBypassKey = key;
|
||||||
|
saveSettings();
|
||||||
|
emit shortcutsChanged();
|
||||||
|
}
|
||||||
|
|
||||||
void ThemeManager::resetFont()
|
void ThemeManager::resetFont()
|
||||||
{
|
{
|
||||||
m_fontFamily.clear();
|
m_fontFamily.clear();
|
||||||
|
|
@ -454,6 +482,13 @@ void ThemeManager::loadSettings()
|
||||||
m_fontStyle = settings.value("Font/style", "").toString();
|
m_fontStyle = settings.value("Font/style", "").toString();
|
||||||
|
|
||||||
m_closeToTray = settings.value("General/closeToTray", false).toBool();
|
m_closeToTray = settings.value("General/closeToTray", false).toBool();
|
||||||
|
|
||||||
|
m_approveBypassKey = settings.value("Shortcuts/approveBypass", "Ctrl+Y").toString();
|
||||||
|
m_denyBypassKey = settings.value("Shortcuts/denyBypass", "Ctrl+N").toString();
|
||||||
|
} else {
|
||||||
|
// Defaults when no config file exists
|
||||||
|
m_approveBypassKey = "Ctrl+Y";
|
||||||
|
m_denyBypassKey = "Ctrl+N";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -469,4 +504,6 @@ void ThemeManager::saveSettings()
|
||||||
settings.setValue("Font/size", m_fontSize);
|
settings.setValue("Font/size", m_fontSize);
|
||||||
settings.setValue("Font/style", m_fontStyle);
|
settings.setValue("Font/style", m_fontStyle);
|
||||||
settings.setValue("General/closeToTray", m_closeToTray);
|
settings.setValue("General/closeToTray", m_closeToTray);
|
||||||
|
settings.setValue("Shortcuts/approveBypass", m_approveBypassKey);
|
||||||
|
settings.setValue("Shortcuts/denyBypass", m_denyBypassKey);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -49,6 +49,10 @@ class ThemeManager : public QObject
|
||||||
// Close-to-tray
|
// Close-to-tray
|
||||||
Q_PROPERTY(bool closeToTray READ closeToTray WRITE setCloseToTray NOTIFY closeToTrayChanged)
|
Q_PROPERTY(bool closeToTray READ closeToTray WRITE setCloseToTray NOTIFY closeToTrayChanged)
|
||||||
|
|
||||||
|
// Shortcut keys
|
||||||
|
Q_PROPERTY(QString approveBypassKey READ approveBypassKey WRITE setApproveBypassKey NOTIFY shortcutsChanged)
|
||||||
|
Q_PROPERTY(QString denyBypassKey READ denyBypassKey WRITE setDenyBypassKey NOTIFY shortcutsChanged)
|
||||||
|
|
||||||
public:
|
public:
|
||||||
enum Mode {
|
enum Mode {
|
||||||
System, // Follow KDE system color scheme
|
System, // Follow KDE system color scheme
|
||||||
|
|
@ -99,6 +103,12 @@ public:
|
||||||
bool closeToTray() const;
|
bool closeToTray() const;
|
||||||
void setCloseToTray(bool enabled);
|
void setCloseToTray(bool enabled);
|
||||||
|
|
||||||
|
// Shortcut keys
|
||||||
|
QString approveBypassKey() const;
|
||||||
|
void setApproveBypassKey(const QString &key);
|
||||||
|
QString denyBypassKey() const;
|
||||||
|
void setDenyBypassKey(const QString &key);
|
||||||
|
|
||||||
Q_INVOKABLE void resetFont();
|
Q_INVOKABLE void resetFont();
|
||||||
Q_INVOKABLE QString chooseFolder(const QString &title, const QString &startDir = QString());
|
Q_INVOKABLE QString chooseFolder(const QString &title, const QString &startDir = QString());
|
||||||
|
|
||||||
|
|
@ -110,6 +120,7 @@ signals:
|
||||||
void paletteChanged();
|
void paletteChanged();
|
||||||
void fontChanged();
|
void fontChanged();
|
||||||
void closeToTrayChanged();
|
void closeToTrayChanged();
|
||||||
|
void shortcutsChanged();
|
||||||
|
|
||||||
private:
|
private:
|
||||||
QPalette paletteFromScheme(const QString &path) const;
|
QPalette paletteFromScheme(const QString &path) const;
|
||||||
|
|
@ -126,6 +137,8 @@ private:
|
||||||
int m_fontSize;
|
int m_fontSize;
|
||||||
QString m_fontStyle;
|
QString m_fontStyle;
|
||||||
bool m_closeToTray;
|
bool m_closeToTray;
|
||||||
|
QString m_approveBypassKey;
|
||||||
|
QString m_denyBypassKey;
|
||||||
QFileSystemWatcher *m_watcher;
|
QFileSystemWatcher *m_watcher;
|
||||||
KConfigWatcher::Ptr m_configWatcher;
|
KConfigWatcher::Ptr m_configWatcher;
|
||||||
static constexpr const char *SCHEMES_DIR = "/usr/share/color-schemes";
|
static constexpr const char *SCHEMES_DIR = "/usr/share/color-schemes";
|
||||||
|
|
|
||||||
|
|
@ -190,7 +190,11 @@ void OllieKateView::ensureAgent()
|
||||||
if (m_sessionId.isEmpty()) {
|
if (m_sessionId.isEmpty()) {
|
||||||
QString cwd = projectBaseDir();
|
QString cwd = projectBaseDir();
|
||||||
if (cwd.isEmpty()) cwd = QDir::homePath();
|
if (cwd.isEmpty()) cwd = QDir::homePath();
|
||||||
m_9p->write("session/new", ("name=kate\ncwd=" + cwd + "\n").toUtf8());
|
QByteArray resp = m_9p->rdwr("session/new", ("name=kate\ncwd=" + cwd + "\n").toUtf8());
|
||||||
|
if (resp.isEmpty()) {
|
||||||
|
qWarning() << "Ollie Kate: failed to create session";
|
||||||
|
return;
|
||||||
|
}
|
||||||
m_sessionId = "kate";
|
m_sessionId = "kate";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue