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
|
||||
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
|
||||
|
|
|
|||
|
|
@ -252,6 +252,10 @@ func (ag *Agent) execOne(ctx context.Context, call backend.ToolCall) (toolResult
|
|||
isErr = true
|
||||
} else {
|
||||
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)})
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -245,6 +245,18 @@ func buildTreeSpec(cfg *Config) virtfs.FsNodeDecl {
|
|||
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.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 <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()
|
||||
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 <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) {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -408,6 +408,23 @@ func (c *ToolsrvConn) ListProcs() ([]byte, error) {
|
|||
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.
|
||||
func (c *ToolsrvConn) readProcInfo(name string) map[string]any {
|
||||
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
|
||||
if streamOut != nil {
|
||||
// Streaming mode: tee to both the stream and local buffer
|
||||
|
|
|
|||
|
|
@ -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: 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.
|
||||
// Called on shutdown to ensure no orphaned processes.
|
||||
func (st *State) KillAll() {
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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.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:
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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";
|
||||
|
|
|
|||
|
|
@ -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";
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue