diff --git a/cmd/olliesrv/internal/agent/agent.go b/cmd/olliesrv/internal/agent/agent.go
index aeba3c0..cf6152f 100644
--- a/cmd/olliesrv/internal/agent/agent.go
+++ b/cmd/olliesrv/internal/agent/agent.go
@@ -64,6 +64,7 @@ type Agent struct {
save func() // trigger debounced persistence
flush func() // immediately flush persistence
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.
chatMu sync.RWMutex
diff --git a/cmd/olliesrv/internal/agent/dispatch.go b/cmd/olliesrv/internal/agent/dispatch.go
index d6fd33b..1438735 100644
--- a/cmd/olliesrv/internal/agent/dispatch.go
+++ b/cmd/olliesrv/internal/agent/dispatch.go
@@ -252,6 +252,10 @@ func (ag *Agent) execOne(ctx context.Context, call backend.ToolCall) (toolResult
isErr = true
} else {
result = fmt.Sprintf("\nid=%d cmd=%q\n", 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)})
diff --git a/cmd/olliesrv/internal/agent/state.go b/cmd/olliesrv/internal/agent/state.go
index f7fb205..41358b1 100644
--- a/cmd/olliesrv/internal/agent/state.go
+++ b/cmd/olliesrv/internal/agent/state.go
@@ -82,6 +82,11 @@ func (ag *Agent) SetOnStateChange(fn func(agentID, state string)) {
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.
func (ag *Agent) setReply(reply string) {
ag.stateMu.Lock()
diff --git a/cmd/olliesrv/internal/fs/spec.go b/cmd/olliesrv/internal/fs/spec.go
index 80190e5..ad7396e 100644
--- a/cmd/olliesrv/internal/fs/spec.go
+++ b/cmd/olliesrv/internal/fs/spec.go
@@ -245,6 +245,18 @@ func buildTreeSpec(cfg *Config) virtfs.FsNodeDecl {
return nil, base, nil
}),
),
+ virtfs.FileNode("event.pub", 0220,
+ virtfs.Doc("Publish event: write topicpayload"),
+ 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.Doc("One-shot LLM generation"),
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) {
return []byte(a.Cwd() + "\n"), nil
}},
- {"proc", "manage background procs: proc [top|term|kill|out|dismiss ]", func(args []string) ([]byte, error) {
+ {"proc", "manage background procs: proc [top|idx|term|kill|out|dismiss ]", func(args []string) ([]byte, error) {
ts := a.ToolServer()
if ts == nil {
return nil, fmt.Errorf("no tool server")
@@ -1122,6 +1134,12 @@ func buildAgentChildren(a *agent.Agent, s *session.Session) []virtfs.FsNodeDecl
}
subcmd := args[0]
switch subcmd {
+ case "idx":
+ data, err := ts.ListProcsIdx()
+ if err != nil {
+ return nil, err
+ }
+ return data, nil
case "term":
if len(args) < 2 {
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
default:
- return nil, fmt.Errorf("unknown proc subcommand: %s (use: top, term , kill , out , dismiss )", subcmd)
+ return nil, fmt.Errorf("unknown proc subcommand: %s (use: top, idx, term , kill , out , dismiss )", subcmd)
}
}},
{"systemprompt", "print the rendered system prompt", func(_ []string) ([]byte, error) {
diff --git a/cmd/olliesrv/internal/fs/support.go b/cmd/olliesrv/internal/fs/support.go
index a72189d..25a2622 100644
--- a/cmd/olliesrv/internal/fs/support.go
+++ b/cmd/olliesrv/internal/fs/support.go
@@ -21,6 +21,11 @@ func wireAgentEvents(sessID string, ag *agent.Agent) {
topic := "session." + sessID + ".agent." + agentID + ".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
diff --git a/cmd/olliesrv/internal/toolclient/toolsrv.go b/cmd/olliesrv/internal/toolclient/toolsrv.go
index 143e21e..2bcdc90 100644
--- a/cmd/olliesrv/internal/toolclient/toolsrv.go
+++ b/cmd/olliesrv/internal/toolclient/toolsrv.go
@@ -408,6 +408,23 @@ func (c *ToolsrvConn) ListProcs() ([]byte, error) {
return io.ReadAll(fid)
}
+// ListProcsIdx returns a machine-readable TSV proc listing.
+// Format: pidstatusexit_codetoolcmdstart_unixruntime_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.
func (c *ToolsrvConn) readProcInfo(name string) map[string]any {
path := fmt.Sprintf("proc/%s/stat", name)
diff --git a/cmd/toolsrv/internal/exec/exec.go b/cmd/toolsrv/internal/exec/exec.go
index 3fe258c..c07a114 100644
--- a/cmd/toolsrv/internal/exec/exec.go
+++ b/cmd/toolsrv/internal/exec/exec.go
@@ -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
if streamOut != nil {
// Streaming mode: tee to both the stream and local buffer
diff --git a/cmd/toolsrv/internal/server/proc.go b/cmd/toolsrv/internal/server/proc.go
index 5d5ebbf..e9ec9b8 100644
--- a/cmd/toolsrv/internal/server/proc.go
+++ b/cmd/toolsrv/internal/server/proc.go
@@ -518,6 +518,40 @@ func (st *State) ListProcsForAgent(agentID string) string {
return buf.String()
}
+// ListProcsIdxForAgent returns a machine-readable TSV list of processes.
+// If agentID is empty, returns all processes.
+// Format: pidstatusexit_codetoolcmdstart_unixruntime_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.
// Called on shutdown to ensure no orphaned processes.
func (st *State) KillAll() {
diff --git a/cmd/toolsrv/internal/server/server.go b/cmd/toolsrv/internal/server/server.go
index 404cb4f..e93bf63 100644
--- a/cmd/toolsrv/internal/server/server.go
+++ b/cmd/toolsrv/internal/server/server.go
@@ -195,6 +195,13 @@ func Spec(srv *Server) virtfs.FsNodeDecl {
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.Doc("Execute tool: write token + tool + args, read result (blocking)"),
virtfs.Rdwr(func(ctx context.Context, data []byte) ([]byte, error) {
diff --git a/cmd/toolsrv/main.go b/cmd/toolsrv/main.go
index db972d7..9cc639a 100644
--- a/cmd/toolsrv/main.go
+++ b/cmd/toolsrv/main.go
@@ -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) {
// Build the interrupt message
proc.Lock()
output := proc.OutputString()
exitCode := proc.ExitCode
cmd := proc.Cmd
+ tool := proc.Tool
pid := proc.ID
sid := proc.SessionID
aid := proc.AgentID
@@ -290,6 +292,15 @@ func pushProcInterrupt(ctx context.Context, proc *server.Proc) {
}
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
promptPath := fmt.Sprintf("session/%s/agent/%s/prompt", sid, aid)
fid, err := fsys.Open(promptPath, plan9.OWRITE)
diff --git a/doc/design/proc-window.md b/doc/design/proc-window.md
new file mode 100644
index 0000000..16ee260
--- /dev/null
+++ b/doc/design/proc-window.md
@@ -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 `
+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:
+ - `pidstatusexit_codetoolcmdstart_unixruntime_sec`
+- Status is `running` or `exited`
+- Exit code is `-1` for running processes
+
+#### Event Publishing Endpoint
+- `event.pub` file at root namespace accepts writes: `topicpayload`
+- 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
+```
diff --git a/doc/event-topics.md b/doc/event-topics.md
index 9041414..e838ebf 100644
--- a/doc/event-topics.md
+++ b/doc/event-topics.md
@@ -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.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
Subscribe to specific events using the streaming rdwr pattern:
diff --git a/kde/gui/ChatPane.qml b/kde/gui/ChatPane.qml
index f0ba59a..b23e382 100644
--- a/kde/gui/ChatPane.qml
+++ b/kde/gui/ChatPane.qml
@@ -620,6 +620,22 @@ Connections {
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
+ }
+ }
}
}
diff --git a/kde/gui/SettingsDialog.qml b/kde/gui/SettingsDialog.qml
index 39ca4a6..57b2b50 100644
--- a/kde/gui/SettingsDialog.qml
+++ b/kde/gui/SettingsDialog.qml
@@ -6,7 +6,7 @@ import QtQuick.Layouts 1.15
Dialog {
id: root
title: "Settings"
- width: 380
+ width: 420
modal: false
x: (parent ? (parent.width - width) / 2 : 0)
y: (parent ? (parent.height - height) / 2 : 0)
@@ -117,6 +117,51 @@ Dialog {
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 {
text: "Font"
font.bold: true
diff --git a/kde/gui/main.qml b/kde/gui/main.qml
index 4176ade..c84c254 100644
--- a/kde/gui/main.qml
+++ b/kde/gui/main.qml
@@ -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 quitting: false
@@ -210,6 +223,7 @@ ApplicationWindow {
}
ChatPane {
+ id: chatPane
SplitView.fillWidth: true
SplitView.fillHeight: true
sidebarVisible: root.sidebarVisible
diff --git a/kde/gui/thememanager.cpp b/kde/gui/thememanager.cpp
index 25ddeee..13107d2 100644
--- a/kde/gui/thememanager.cpp
+++ b/kde/gui/thememanager.cpp
@@ -394,6 +394,34 @@ void ThemeManager::setCloseToTray(bool enabled)
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()
{
m_fontFamily.clear();
@@ -454,6 +482,13 @@ void ThemeManager::loadSettings()
m_fontStyle = settings.value("Font/style", "").toString();
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/style", m_fontStyle);
settings.setValue("General/closeToTray", m_closeToTray);
+ settings.setValue("Shortcuts/approveBypass", m_approveBypassKey);
+ settings.setValue("Shortcuts/denyBypass", m_denyBypassKey);
}
diff --git a/kde/gui/thememanager.h b/kde/gui/thememanager.h
index 8a8a26a..00a6b3d 100644
--- a/kde/gui/thememanager.h
+++ b/kde/gui/thememanager.h
@@ -49,6 +49,10 @@ class ThemeManager : public QObject
// Close-to-tray
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:
enum Mode {
System, // Follow KDE system color scheme
@@ -99,6 +103,12 @@ public:
bool closeToTray() const;
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 QString chooseFolder(const QString &title, const QString &startDir = QString());
@@ -110,6 +120,7 @@ signals:
void paletteChanged();
void fontChanged();
void closeToTrayChanged();
+ void shortcutsChanged();
private:
QPalette paletteFromScheme(const QString &path) const;
@@ -126,6 +137,8 @@ private:
int m_fontSize;
QString m_fontStyle;
bool m_closeToTray;
+ QString m_approveBypassKey;
+ QString m_denyBypassKey;
QFileSystemWatcher *m_watcher;
KConfigWatcher::Ptr m_configWatcher;
static constexpr const char *SCHEMES_DIR = "/usr/share/color-schemes";
diff --git a/kde/kate/ollie_kate.cpp b/kde/kate/ollie_kate.cpp
index d9f6854..518fd91 100644
--- a/kde/kate/ollie_kate.cpp
+++ b/kde/kate/ollie_kate.cpp
@@ -190,7 +190,11 @@ void OllieKateView::ensureAgent()
if (m_sessionId.isEmpty()) {
QString cwd = projectBaseDir();
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";
}