mako QoL update
This commit is contained in:
@@ -90,7 +90,8 @@ eventbox, box, label, scale {
|
||||
.battery-icon,
|
||||
.battery-percent,
|
||||
.time-label,
|
||||
.notification-icon {
|
||||
.notification-icon,
|
||||
.dnd-icon {
|
||||
color: $fg;
|
||||
font-weight: bold;
|
||||
padding: 0 $icon-pad;
|
||||
@@ -101,7 +102,8 @@ eventbox, box, label, scale {
|
||||
|
||||
.signal-eventbox,
|
||||
.net-eventbox,
|
||||
.notification-eventbox {
|
||||
.notification-eventbox,
|
||||
.dnd-eventbox {
|
||||
min-width: 0;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
@@ -70,11 +70,18 @@
|
||||
;; Notifications
|
||||
;; ---------------------------------------------------------------------------
|
||||
|
||||
(defpoll dnd-state :interval "1s" "bash ~/.config/eww/scripts/mako-dnd-state")
|
||||
|
||||
(defwidget notification-widget []
|
||||
(box :orientation "h" :space-evenly false :spacing gap-tight
|
||||
(eventbox :class "notification-eventbox"
|
||||
:onclick "bash ~/.config/eww/scripts/notification-history-toggle"
|
||||
:tooltip "Toggle notification history"
|
||||
(label :text "" :class "notification-icon")))
|
||||
(label :text "" :class "notification-icon"))
|
||||
(eventbox :class "notification-eventbox dnd-eventbox"
|
||||
:onclick "bash ~/.config/eww/scripts/mako-dnd-toggle"
|
||||
:tooltip "Toggle do-not-disturb"
|
||||
(label :text {dnd-state} :class "notification-icon dnd-icon"))))
|
||||
|
||||
;; ---------------------------------------------------------------------------
|
||||
;; Battery
|
||||
|
||||
8
eww/.config/eww/scripts/mako-dnd-state
Executable file
8
eww/.config/eww/scripts/mako-dnd-state
Executable file
@@ -0,0 +1,8 @@
|
||||
#!/bin/bash
|
||||
# Print the current do-not-disturb state as a bar icon.
|
||||
# Called by eww as a defpoll.
|
||||
if makoctl mode 2>/dev/null | grep -q dnd; then
|
||||
echo "" # bell-off: do-not-disturb active
|
||||
else
|
||||
echo "" # bell-ring: notifications on
|
||||
fi
|
||||
3
eww/.config/eww/scripts/mako-dnd-toggle
Executable file
3
eww/.config/eww/scripts/mako-dnd-toggle
Executable file
@@ -0,0 +1,3 @@
|
||||
#!/bin/bash
|
||||
# Toggle do-not-disturb mode for mako notifications.
|
||||
makoctl mode -t dnd
|
||||
@@ -14,6 +14,7 @@ height=300
|
||||
margin=5
|
||||
padding=8
|
||||
icon-path=/usr/share/icons/Papirus-Dark
|
||||
max-icon-size=48
|
||||
|
||||
# Layout
|
||||
anchor=top-right
|
||||
@@ -21,29 +22,66 @@ outer-margin=30,0,0,0
|
||||
|
||||
# Behavior
|
||||
default-timeout=6000
|
||||
ignore-timeout=0
|
||||
max-visible=20
|
||||
# Force mako's own timeouts so no notification (critical or not) can hang
|
||||
# indefinitely, even if the app requests timeout=0 (never).
|
||||
ignore-timeout=1
|
||||
max-visible=5
|
||||
sort=-time
|
||||
group-by=app-name
|
||||
max-history=20
|
||||
|
||||
# Format - show app name in italics
|
||||
format=<b>%s</b> <i>(%a)</i>\n%b
|
||||
markup=1
|
||||
# Progress bar for notifications that report progress (downloads, updates)
|
||||
progress-color=over #69C3FF
|
||||
|
||||
# Mouse bindings - click to focus app, invoke action, and close
|
||||
# Format - plain text (markup=0) so app text with <tag> renders literally.
|
||||
# Priority is shown by a leading icon instead of markup/colors.
|
||||
markup=0
|
||||
format= %s (%a)\n%b
|
||||
|
||||
# Mouse bindings
|
||||
on-button-left=exec ~/.config/mako/scripts/handle-click $id
|
||||
on-button-middle=none
|
||||
on-button-right=exec makoctl dismiss -n $id
|
||||
on-notify=exec ~/.config/mako/scripts/notify-sound $id
|
||||
|
||||
# Urgency levels
|
||||
[urgency=low]
|
||||
background-color=#212a3b
|
||||
border-color=#405275
|
||||
format= %s (%a)\n%b
|
||||
|
||||
[urgency=normal]
|
||||
background-color=#1c2433
|
||||
border-color=#69C3FF
|
||||
format= %s (%a)\n%b
|
||||
|
||||
[urgency=critical]
|
||||
background-color=#1c2433
|
||||
border-color=#FF738A
|
||||
default-timeout=0
|
||||
format= %s (%a)\n%b
|
||||
# Was 0 (never) - critical notifications now auto-dismiss after 15s
|
||||
default-timeout=15000
|
||||
|
||||
# Grouped notifications (group-by=app-name) - plain text, icon per urgency
|
||||
[grouped]
|
||||
format= (%g) %s\n%b
|
||||
|
||||
[grouped urgency=low]
|
||||
format= (%g) %s\n%b
|
||||
|
||||
[grouped urgency=critical]
|
||||
format= (%g) %s\n%b
|
||||
|
||||
# Placeholder shown when more notifications exist than max-visible
|
||||
[hidden]
|
||||
format= +%h more
|
||||
|
||||
# Pi agent notifications (sent via the pi QoL extension from wezterm)
|
||||
[app-name=wezterm summary=Pi]
|
||||
border-color=#EACD61
|
||||
background-color=#212a3b
|
||||
|
||||
# Do-not-disturb: makoctl mode -a dnd hides all notifications.
|
||||
# Must stay last so it overrides every style above.
|
||||
[mode=dnd]
|
||||
invisible=1
|
||||
|
||||
@@ -14,6 +14,7 @@ height=300
|
||||
margin=5
|
||||
padding=8
|
||||
icon-path=/usr/share/icons/Papirus-Dark
|
||||
max-icon-size=48
|
||||
|
||||
# Layout
|
||||
anchor=top-center
|
||||
@@ -21,29 +22,67 @@ outer-margin=30,0,0,0
|
||||
|
||||
# Behavior
|
||||
default-timeout=6000
|
||||
ignore-timeout=0
|
||||
max-visible=20
|
||||
# Force mako's own timeouts so no notification (critical or not) can hang
|
||||
# indefinitely, even if the app requests timeout=0 (never).
|
||||
ignore-timeout=1
|
||||
max-visible=5
|
||||
sort=-time
|
||||
group-by=app-name
|
||||
max-history=20
|
||||
|
||||
# Format - show app name in italics
|
||||
format=<b>%s</b> <i>(%a)</i>\n%b
|
||||
markup=1
|
||||
# Progress bar for notifications that report progress (downloads, updates)
|
||||
progress-color=over #$BORDER
|
||||
|
||||
# Mouse bindings - click to focus app, invoke action, and close
|
||||
# Format - plain text (markup=0) so app text with <tag> renders literally.
|
||||
# Priority is shown by a leading icon instead of markup/colors.
|
||||
markup=0
|
||||
format= %s (%a)\n%b
|
||||
|
||||
# Mouse bindings
|
||||
on-button-left=exec ~/.config/mako/scripts/handle-click $id
|
||||
on-button-middle=none
|
||||
on-button-right=exec makoctl dismiss -n $id
|
||||
on-notify=exec ~/.config/mako/scripts/notify-sound $id
|
||||
|
||||
# Urgency levels
|
||||
[urgency=low]
|
||||
background-color=#$BG_BAR
|
||||
border-color=#$TRACK
|
||||
format= %s (%a)\n%b
|
||||
|
||||
[urgency=normal]
|
||||
background-color=#$BG
|
||||
border-color=#$BORDER
|
||||
format= %s (%a)\n%b
|
||||
|
||||
[urgency=critical]
|
||||
background-color=#$BG
|
||||
border-color=#$URGENT
|
||||
default-timeout=0
|
||||
format= %s (%a)\n%b
|
||||
# Was 0 (never) - critical notifications now auto-dismiss after 15s
|
||||
default-timeout=15000
|
||||
|
||||
# Grouped notifications (group-by=app-name) - plain text, icon per urgency
|
||||
[grouped]
|
||||
format= (%g) %s\n%b
|
||||
|
||||
[grouped urgency=low]
|
||||
format= (%g) %s\n%b
|
||||
|
||||
[grouped urgency=critical]
|
||||
format= (%g) %s\n%b
|
||||
|
||||
# Placeholder shown when more notifications exist than max-visible
|
||||
[hidden]
|
||||
format= +%h more
|
||||
|
||||
# Pi agent notifications (sent via the pi QoL extension from wezterm).
|
||||
# Matches theme accent $accent (#EACD61).
|
||||
[app-name=wezterm summary=Pi]
|
||||
border-color=#EACD61
|
||||
background-color=#$BG_BAR
|
||||
|
||||
# Do-not-disturb: makoctl mode -a dnd hides all notifications.
|
||||
# Must stay last so it overrides every style above.
|
||||
[mode=dnd]
|
||||
invisible=1
|
||||
|
||||
BIN
mako/.config/mako/scripts/Chord.wav
Normal file
BIN
mako/.config/mako/scripts/Chord.wav
Normal file
Binary file not shown.
BIN
mako/.config/mako/scripts/Chord2.wav
Normal file
BIN
mako/.config/mako/scripts/Chord2.wav
Normal file
Binary file not shown.
64
mako/.config/mako/scripts/handle-click
Normal file → Executable file
64
mako/.config/mako/scripts/handle-click
Normal file → Executable file
@@ -1,43 +1,47 @@
|
||||
#!/bin/bash
|
||||
# Handle notification click: focus app → invoke action → close
|
||||
# Called by mako with notification ID as parameter
|
||||
# Handle notification click: focus the app that sent it → invoke action → dismiss
|
||||
# Called by mako with the notification ID as argument
|
||||
|
||||
ID="$1"
|
||||
[ -z "$ID" ] && exit 0
|
||||
|
||||
if [ -z "$ID" ]; then
|
||||
exit 0
|
||||
fi
|
||||
# Pull the app name out of makoctl list, scoped to this notification ID
|
||||
APP=$(makoctl list 2>/dev/null | awk -v id="$ID" '
|
||||
$1 == "Notification" { if (seen) exit; if ($2 == id) seen = 1; next }
|
||||
seen && /^ App name:/ { sub(/^ App name: /, ""); print; exit }')
|
||||
|
||||
# Get app name from notification using makoctl
|
||||
APPNAME=$(makoctl list | grep -A 2 "^Notification $ID" | grep "App name:" | cut -d: -f2- | xargs)
|
||||
# Find a sway window matching the app and focus it.
|
||||
# Exact app_id match first, then X11 class, then partial app_id as fallback.
|
||||
if [ -n "$APP" ]; then
|
||||
TREE=$(swaymsg -t get_tree 2>/dev/null)
|
||||
|
||||
# Step 1: Focus the application window in Sway (if app name found)
|
||||
if [ -n "$APPNAME" ]; then
|
||||
# Get the workspace of the window
|
||||
WORKSPACE=$(swaymsg -t get_tree | jq -r ".. | select(.app_id?==\"$APPNAME\" or .window_properties?.class?==\"$APPNAME\") | .workspace" | head -1)
|
||||
CID=$(echo "$TREE" | jq -r --arg app "$APP" '
|
||||
[ .. | objects | select(.type? == "con")
|
||||
| select((.app_id? // "") != "")
|
||||
| select((.app_id | ascii_downcase) == ($app | ascii_downcase))
|
||||
| .id ][0] // empty')
|
||||
|
||||
# If workspace not found, try case-insensitive
|
||||
if [ -z "$WORKSPACE" ] || [ "$WORKSPACE" = "null" ]; then
|
||||
WORKSPACE=$(swaymsg -t get_tree | jq -r ".. | select(.app_id?|test(\"$APPNAME\";\"i\") or .window_properties?.class?|test(\"$APPNAME\";\"i\")) | .workspace" | head -1)
|
||||
if [ -z "$CID" ]; then
|
||||
CID=$(echo "$TREE" | jq -r --arg app "$APP" '
|
||||
[ .. | objects | select(.type? == "con")
|
||||
| select((.window_properties?.class? // "") != "")
|
||||
| select((.window_properties.class | ascii_downcase) == ($app | ascii_downcase))
|
||||
| .id ][0] // empty')
|
||||
fi
|
||||
|
||||
# Switch to the workspace if found
|
||||
if [ -n "$WORKSPACE" ] && [ "$WORKSPACE" != "null" ]; then
|
||||
swaymsg workspace "$WORKSPACE" 2>/dev/null
|
||||
sleep 0.1
|
||||
if [ -z "$CID" ]; then
|
||||
CID=$(echo "$TREE" | jq -r --arg app "$APP" '
|
||||
[ .. | objects | select(.type? == "con")
|
||||
| select((.app_id? // "") != "")
|
||||
| select((.app_id | ascii_downcase) | contains($app | ascii_downcase))
|
||||
| .id ][0] // empty')
|
||||
fi
|
||||
|
||||
# Now focus the window
|
||||
swaymsg "[app_id=\"$APPNAME\"]" focus 2>/dev/null || \
|
||||
swaymsg "[app_id=\"(?i)$APPNAME\"]" focus 2>/dev/null || \
|
||||
swaymsg "[class=\"$APPNAME\"]" focus 2>/dev/null || \
|
||||
swaymsg "[class=\"(?i)$APPNAME\"]" focus 2>/dev/null
|
||||
|
||||
sleep 0.1
|
||||
if [ -n "$CID" ]; then
|
||||
swaymsg "[con_id=$CID]" focus >/dev/null 2>&1
|
||||
fi
|
||||
fi
|
||||
|
||||
# Step 2: Invoke the notification's default action (if any)
|
||||
makoctl invoke -n "$ID" 2>/dev/null
|
||||
|
||||
# Step 3: Dismiss the notification
|
||||
makoctl dismiss -n "$ID" 2>/dev/null
|
||||
# Invoke the notification's default action (may open/focus the app), then dismiss
|
||||
makoctl invoke -n "$ID" >/dev/null 2>&1
|
||||
makoctl dismiss -n "$ID" >/dev/null 2>&1
|
||||
|
||||
39
mako/.config/mako/scripts/notify-sound
Executable file
39
mako/.config/mako/scripts/notify-sound
Executable file
@@ -0,0 +1,39 @@
|
||||
#!/bin/bash
|
||||
# Play a notification sound, alternating between Chord.wav and Chord2.wav.
|
||||
# Skips: do-not-disturb mode and low-urgency notifications.
|
||||
# Usage: notify-sound <notification-id> (called from mako's on-notify binding)
|
||||
|
||||
ID="${1:-}"
|
||||
|
||||
# Silence while do-not-disturb is active
|
||||
if makoctl mode 2>/dev/null | grep -q dnd; then
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Only play for normal/critical urgency
|
||||
if [ -n "$ID" ]; then
|
||||
urgency=$(makoctl list 2>/dev/null | awk -v id="$ID" '
|
||||
$1 == "Notification" { if (seen) exit; if ($2 == id) seen = 1; next }
|
||||
seen && /^ Urgency:/ { print $2; exit }')
|
||||
if [ "$urgency" = "low" ]; then
|
||||
exit 0
|
||||
fi
|
||||
fi
|
||||
|
||||
SOUND_DIR=~/.config/mako/scripts
|
||||
STATE_FILE="${XDG_CACHE_HOME:-$HOME/.cache}/mako-notify-sound"
|
||||
|
||||
# Alternate between the two sounds on every play
|
||||
if [ -f "$STATE_FILE" ] && [ "$(cat "$STATE_FILE")" = "0" ]; then
|
||||
SOUND="$SOUND_DIR/Chord2.wav"
|
||||
echo "1" > "$STATE_FILE"
|
||||
else
|
||||
SOUND="$SOUND_DIR/Chord.wav"
|
||||
echo "0" > "$STATE_FILE"
|
||||
fi
|
||||
|
||||
if command -v paplay >/dev/null 2>&1; then
|
||||
paplay "$SOUND"
|
||||
elif command -v pw-play >/dev/null 2>&1; then
|
||||
pw-play "$SOUND"
|
||||
fi
|
||||
BIN
mako/.config/mako/scripts/pling.wav
Normal file
BIN
mako/.config/mako/scripts/pling.wav
Normal file
Binary file not shown.
114
pi/.pi/agent/extensions/subagent-pane-reminder.ts
Normal file
114
pi/.pi/agent/extensions/subagent-pane-reminder.ts
Normal file
@@ -0,0 +1,114 @@
|
||||
/**
|
||||
* Subagent pane lifecycle reminder.
|
||||
*
|
||||
* When the parent agent dispatches to a `pane: true` agent via `subagent`,
|
||||
* this extension floats a cleanup reminder to the parent only:
|
||||
*
|
||||
* - immediately: appended to the `subagent` tool result of that call;
|
||||
* - persistently: a one-line system-prompt note on later turns until the
|
||||
* parent calls `stop_subagent` for that agent (or the session ends).
|
||||
*
|
||||
* Nothing is injected for bg agents (`pane: false`), for `delegate_subagent`
|
||||
* (pane targets are rejected there anyway), for `/agents` commands, or in
|
||||
* sessions that never spawn a pane agent. Other sessions and workflows stay
|
||||
* untouched.
|
||||
*/
|
||||
|
||||
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
||||
import * as fs from "node:fs";
|
||||
import * as path from "node:path";
|
||||
import { homedir } from "node:os";
|
||||
|
||||
interface SubagentInput {
|
||||
agent?: unknown;
|
||||
tasks?: Array<{ agent?: unknown }>;
|
||||
chain?: Array<{ agent?: unknown }>;
|
||||
}
|
||||
|
||||
const USER_AGENT_DIRS: string[] = [
|
||||
path.join(homedir(), ".pi", "agent", "agents"),
|
||||
path.join(homedir(), ".claude", "agents"),
|
||||
];
|
||||
|
||||
function projectAgentDirs(cwd: string): string[] {
|
||||
const dirs: string[] = [];
|
||||
let current = path.resolve(cwd);
|
||||
for (;;) {
|
||||
dirs.push(path.join(current, ".pi", "agents"), path.join(current, ".claude", "agents"));
|
||||
const parent = path.dirname(current);
|
||||
if (parent === current) break;
|
||||
current = parent;
|
||||
}
|
||||
return dirs;
|
||||
}
|
||||
|
||||
function hasPaneFrontmatter(text: string): boolean {
|
||||
const block = text.match(/^---\r?\n([\s\S]*?)\r?\n---/);
|
||||
return block ? /^\s*pane\s*:\s*(?:true|yes|on)\s*$/im.test(block[1]) : false;
|
||||
}
|
||||
|
||||
function isPaneAgent(name: string, cwd: string): boolean {
|
||||
for (const dir of [...USER_AGENT_DIRS, ...projectAgentDirs(cwd)]) {
|
||||
for (const ext of [".md", ".markdown"]) {
|
||||
try {
|
||||
if (hasPaneFrontmatter(fs.readFileSync(path.join(dir, `${name}${ext}`), "utf8"))) return true;
|
||||
} catch {
|
||||
// not present in this directory
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
function agentNamesFrom(input: SubagentInput): string[] {
|
||||
const names = new Set<string>();
|
||||
const add = (value: unknown) => {
|
||||
if (typeof value === "string" && value.trim()) names.add(value.trim());
|
||||
};
|
||||
add(input.agent);
|
||||
for (const item of input.tasks ?? []) add(item.agent);
|
||||
for (const item of input.chain ?? []) add(item.agent);
|
||||
return [...names];
|
||||
}
|
||||
|
||||
function reminderText(names: string[]): string {
|
||||
const quoted = names.map((name) => `\`${name}\``).join(", ");
|
||||
return (
|
||||
`\n\nPane cleanup: you spawned persistent pane agent(s) ${quoted}. ` +
|
||||
`After you collect the result, call \`stop_subagent\` with the agent name to kill its tmux pane. ` +
|
||||
`Skip only if you plan a follow-up (steer or another subagent call to the same agent); stop it at the end of the workflow. ` +
|
||||
`Stopping preserves the session file, so a later \`subagent\` call resumes the agent's memory.`
|
||||
);
|
||||
}
|
||||
|
||||
export default function (pi: ExtensionAPI) {
|
||||
// Agent names whose panes are still alive this session.
|
||||
const pending = new Set<string>();
|
||||
|
||||
pi.on("tool_call", (event) => {
|
||||
if (event.toolName === "stop_subagent") {
|
||||
const agent = (event.input as { agent?: unknown } | undefined)?.agent;
|
||||
if (typeof agent === "string") pending.delete(agent);
|
||||
}
|
||||
});
|
||||
|
||||
pi.on("tool_result", (event, ctx) => {
|
||||
if (event.toolName !== "subagent" || event.isError) return;
|
||||
const input = (event.input as SubagentInput | undefined) ?? {};
|
||||
const paneAgents = agentNamesFrom(input).filter((name) => isPaneAgent(name, ctx.cwd));
|
||||
if (paneAgents.length === 0) return;
|
||||
for (const name of paneAgents) pending.add(name);
|
||||
return {
|
||||
content: [...(event.content ?? []), { type: "text" as const, text: reminderText(paneAgents) }],
|
||||
};
|
||||
});
|
||||
|
||||
pi.on("before_agent_start", (event) => {
|
||||
if (pending.size === 0) return;
|
||||
const note =
|
||||
"\n\nPane cleanup (pending): " +
|
||||
`[${[...pending].join(", ")}]. ` +
|
||||
"After collecting their results, call stop_subagent for each (unless a follow-up is planned).";
|
||||
return { systemPrompt: event.systemPrompt + note };
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user