Compare commits

..

13 Commits

Author SHA1 Message Date
thomas 534ec8b99f cursor extension 2026-04-16 11:55:57 +01:00
thomas c004356b5a zellij cleanup + small files 2026-04-16 09:48:44 +01:00
thomas 8fa80f58ea agents md to skip validations 2026-04-16 09:25:54 +01:00
thomas b42a9ecffa jj workspaces skill 2026-04-16 09:12:51 +01:00
thomas 966e40e71b anthropic fix 2026-04-13 15:52:17 +01:00
thomas 4af7031922 fix screenshots 2026-04-13 15:49:09 +01:00
thomas 6003f41a12 notify even when peon muted 2026-04-09 10:09:45 +01:00
thomas 587c54060b fix zellij 2026-04-08 10:59:18 +01:00
thomas cad0540600 fix biome 2026-04-07 17:29:51 +01:00
thomas fd2307eb0c fix autocomplete 2026-04-07 17:27:37 +01:00
thomas 6d525d0971 fix git blame lol 2026-04-07 16:48:17 +01:00
thomas 51073c07a8 update pi 2026-04-01 15:29:36 +01:00
thomas e4b6fbabc6 slow tool settings 2026-03-31 14:16:18 +01:00
21 changed files with 2282 additions and 69 deletions
+14
View File
@@ -194,6 +194,17 @@
"centeringMode": "index",
"clockDateFormat": "d MMM yyyy",
"lockDateFormat": "",
"greeterRememberLastSession": true,
"greeterRememberLastUser": true,
"greeterEnableFprint": false,
"greeterEnableU2f": false,
"greeterWallpaperPath": "",
"greeterUse24HourClock": true,
"greeterShowSeconds": false,
"greeterPadHours12Hour": false,
"greeterLockDateFormat": "",
"greeterFontFamily": "",
"greeterWallpaperFillMode": "",
"mediaSize": 1,
"appLauncherViewMode": "list",
"spotlightModalViewMode": "list",
@@ -314,6 +325,7 @@
"matugenTemplateKcolorscheme": true,
"matugenTemplateVscode": true,
"matugenTemplateEmacs": true,
"matugenTemplateZed": true,
"showDock": false,
"dockAutoHide": false,
"dockSmartAutoHide": false,
@@ -355,6 +367,8 @@
"lockAtStartup": false,
"enableFprint": false,
"maxFprintTries": 3,
"enableU2f": false,
"u2fMode": "or",
"lockScreenActiveMonitor": "all",
"lockScreenInactiveColor": "#000000",
"lockScreenNotificationMode": 0,
+1 -1
View File
@@ -1,3 +1,3 @@
if test (uname) = Darwin
fnm env --use-on-cd --shell fish | source
fnm env --use-on-cd --log-level=quiet --shell fish | source
end
+11 -2
View File
@@ -102,9 +102,18 @@ status is-interactive; and begin
end
# Add user local bin to PATH
# PATH ordering: prefer Nix/system binaries over self-installed shims in ~/.local/bin
if test (uname) = Linux
fish_add_path -m /run/current-system/sw/bin
end
# Add user local bin to PATH, but keep it after system paths on Linux
if test -d "$HOME/.local/bin"
fish_add_path "$HOME/.local/bin"
if test (uname) = Linux
fish_add_path -a -m "$HOME/.local/bin"
else
fish_add_path "$HOME/.local/bin"
end
end
# pnpm
+19 -7
View File
@@ -1,8 +1,11 @@
#!/usr/bin/env bash
set -u
screenshot_dir="$HOME/Pictures/Screenshots"
remote_target="mac-attio:~/screenshot.png"
timeout=3 # seconds
file_timeout=8 # seconds to wait for screenshot file to appear
upload_timeout=10 # seconds
notify() {
DBUS_SESSION_BUS_ADDRESS="unix:path=/run/user/$(id -u)/bus" \
@@ -15,12 +18,13 @@ shopt -s nullglob
existing_files=("$screenshot_dir"/*.png)
existing_count=${#existing_files[@]}
# Take screenshot
niri msg action screenshot
# Take screenshot (no timeout here so interactive capture isn't canceled)
niri msg action screenshot >/dev/null 2>&1
# Wait for new file (timeout in 0.1s intervals)
deadline=$((timeout * 10))
deadline=$((file_timeout * 10))
count=0
files=("$screenshot_dir"/*.png)
while (( count < deadline )); do
files=("$screenshot_dir"/*.png)
@@ -37,12 +41,20 @@ if (( ${#files[@]} <= existing_count )); then
fi
# Get the new file (most recent)
latest_file=$(ls -1t "${files[@]}" | head -n 1)
latest_file=$(ls -1t -- "${files[@]}" | head -n 1)
# Small delay to ensure file is fully written
sleep 0.1
# Upload
if scp -q "$latest_file" "$remote_target"; then
# Upload with strict SSH options so it never blocks waiting for prompts
if timeout "${upload_timeout}s" scp -q \
-o BatchMode=yes \
-o ConnectTimeout=5 \
-o ConnectionAttempts=1 \
-o ServerAliveInterval=2 \
-o ServerAliveCountMax=1 \
-- "$latest_file" "$remote_target"; then
notify "Screenshot" "Uploaded to Mac"
else
notify "Screenshot" "Upload to Mac failed"
fi
+1 -1
View File
@@ -1,5 +1,5 @@
return {
cmd = { "biome", "lsp-proxy" },
cmd = { "npx", "biome", "lsp-proxy" },
filetypes = {
"javascript",
"javascriptreact",
+25 -1
View File
@@ -32,6 +32,23 @@ return {
},
"folke/lazydev.nvim",
},
config = function(_, opts)
-- Monkey-patch blink's text_edits.get_from_item to clamp textEdit ranges
-- that extend past the cursor. Workaround for tsgo sending bad ranges
-- that eat text (e.g. in JSX string attributes like className="...").
local text_edits = require("blink.cmp.lib.text_edits")
local original_get_from_item = text_edits.get_from_item
text_edits.get_from_item = function(item)
local text_edit = original_get_from_item(item)
local cursor_col = require("blink.cmp.completion.trigger.context").get_cursor()[2]
if text_edit.range and text_edit.range["end"].character > cursor_col then
text_edit.range["end"].character = cursor_col
end
return text_edit
end
require("blink.cmp").setup(opts)
end,
--- @module 'blink.cmp'
--- @type blink.cmp.Config
opts = {
@@ -76,9 +93,16 @@ return {
},
sources = {
default = { "lsp", "path", "snippets", "lazydev" },
default = { "lsp", "path", "snippets", "lazydev", "minuet" },
providers = {
lazydev = { module = "lazydev.integrations.blink", score_offset = 100 },
minuet = {
name = "minuet",
module = "minuet.blink",
async = true,
timeout_ms = 3000,
score_offset = 50,
},
},
},
+2 -2
View File
@@ -6,12 +6,12 @@ return {
-- Allows extra capabilities provided by blink.cmp
{
"saghen/blink.cmp",
config = function(_, opts)
require("blink.cmp").setup(opts)
opts = function(_, opts)
-- Add blink.cmp capabilities to the default LSP client capabilities
vim.lsp.config("*", {
capabilities = require("blink.cmp").get_lsp_capabilities(),
})
return opts
end,
},
+9 -27
View File
@@ -28,34 +28,16 @@ return {
end,
},
{ "nvim-lua/plenary.nvim" },
-- optional, if you are using virtual-text frontend, blink is not required.
-- Minuet blink.cmp integration (merged into main blink.lua spec via opts)
{
"Saghen/blink.cmp",
config = function()
require("blink-cmp").setup({
keymap = {
-- Manually invoke minuet completion.
["<A-y>"] = require("minuet").make_blink_map(),
},
sources = {
-- Enable minuet for autocomplete
default = { "lsp", "path", "buffer", "snippets", "minuet" },
-- For manual completion only, remove 'minuet' from default
providers = {
minuet = {
name = "minuet",
module = "minuet.blink",
async = true,
-- Should match minuet.config.request_timeout * 1000,
-- since minuet.config.request_timeout is in seconds
timeout_ms = 3000,
score_offset = 50, -- Gives minuet higher priority among suggestions
},
},
},
-- Recommended to avoid unnecessary request
completion = { trigger = { prefetch_on_insert = false } },
})
"saghen/blink.cmp",
opts = function(_, opts)
opts.keymap = opts.keymap or {}
opts.keymap["<A-y>"] = require("minuet").make_blink_map()
opts.completion = opts.completion or {}
opts.completion.trigger = opts.completion.trigger or {}
opts.completion.trigger.prefetch_on_insert = false
return opts
end,
},
}
+1 -1
View File
@@ -1,7 +1,7 @@
{
"activePack": "glados",
"volume": 1,
"muted": false,
"muted": true,
"enabledCategories": {
"session.start": true,
"task.acknowledge": true,
+1 -1
View File
@@ -1,7 +1,7 @@
{
"activePack": "solid_snake",
"volume": 1.5,
"muted": false,
"muted": true,
"enabledCategories": {
"session.start": true,
"task.acknowledge": true,
+9 -5
View File
@@ -1,11 +1,15 @@
{
"lastChangelogVersion": "0.63.1",
"defaultProvider": "openai-codex",
"defaultModel": "gpt-5.3-codex",
"defaultThinkingLevel": "high",
"lastChangelogVersion": "0.67.3",
"defaultProvider": "cursor-acp",
"defaultModel": "auto",
"defaultThinkingLevel": "medium",
"theme": "matugen",
"lsp": {
"hookMode": "edit_write"
},
"hideThinkingBlock": false
"hideThinkingBlock": false,
"slowtool": {
"timeoutSeconds": 300,
"enabled": true
}
}
+8 -4
View File
@@ -1,11 +1,15 @@
{
"lastChangelogVersion": "0.57.1",
"defaultProvider": "anthropic",
"defaultModel": "claude-opus-4-6",
"lastChangelogVersion": "0.66.1",
"defaultProvider": "openai-codex",
"defaultModel": "gpt-5.4",
"defaultThinkingLevel": "medium",
"theme": "matugen",
"lsp": {
"hookMode": "edit_write"
},
"hideThinkingBlock": false
"hideThinkingBlock": true,
"slowtool": {
"timeoutSeconds": 300,
"enabled": true
}
}
+6
View File
@@ -1,3 +1,9 @@
# Validations
Sometimes some repositories (stupidly) ask you to run validations after changes or some shit. Thing is, you're smart. Your edit tools already contain formatting and LSP hooks. So, you may ask the user if they want you to run said "required" validations, but they're not really required.
---
# Screenshots
When the user provides a screenshot path (e.g., `/tmp/pi-clipboard-xxx.png`), **ALWAYS** use the `read` tool to read the image file. Do NOT assume you can see the screenshot contents without reading it first.
@@ -0,0 +1,14 @@
import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
const TARGET = "about pi itself, its SDK, extensions, themes, skills, or TUI";
const REPLACEMENT = "about pi, its SDK, extensions, themes, skills, or TUI";
export default function(pi: ExtensionAPI) {
pi.on("before_agent_start", (event, ctx) => {
if (ctx.model?.provider !== "anthropic") return;
if (!event.systemPrompt.includes(TARGET)) return;
return {
systemPrompt: event.systemPrompt.replace(TARGET, REPLACEMENT),
};
});
}
File diff suppressed because it is too large Load Diff
+17 -16
View File
@@ -308,9 +308,24 @@ function pickSound(categoryConfig: CategoryConfig, category: Category): Sound |
// ============ SOUND PLAYBACK ============
function play(category: Category): void {
if (config.muted) return;
if (!config.enabledCategories[category]) return;
const notificationMessages: Record<Category, { title: string; message: string } | null> = {
"session.start": null,
"task.acknowledge": null,
"task.complete": { title: "Pi", message: "Task complete" },
"task.error": { title: "Pi", message: "Task failed" },
"input.required": { title: "Pi", message: "Input required" },
"resource.limit": { title: "Pi", message: "Rate limited" },
};
const notification = notificationMessages[category];
if (notification) {
sendNotification(notification.title, notification.message);
}
if (config.muted) return;
const now = Date.now();
if (now - lastPlayed < DEBOUNCE_MS) {
return;
@@ -345,20 +360,6 @@ function play(category: Category): void {
}
playSound(soundPath, config.volume);
const notificationMessages: Record<Category, { title: string; message: string } | null> = {
"session.start": null,
"task.acknowledge": null,
"task.complete": { title: "Pi", message: "Task complete" },
"task.error": { title: "Pi", message: "Task failed" },
"input.required": { title: "Pi", message: "Input required" },
"resource.limit": { title: "Pi", message: "Rate limited" },
};
const notification = notificationMessages[category];
if (notification) {
sendNotification(notification.title, notification.message);
}
}
// ============ COMMANDS ============
@@ -814,7 +815,7 @@ async function showTestMenu(ctx: ExtensionCommandContext) {
const INTERACTIVE_TOOLS = new Set(["question", "questionnaire"]);
export default function (pi: ExtensionAPI) {
export default function(pi: ExtensionAPI) {
registerCommands(pi);
pi.on("session_start", async (_event, ctx) => {
+80
View File
@@ -14,6 +14,9 @@
*/
import type { ExtensionAPI, ExtensionCommandContext } from "@mariozechner/pi-coding-agent";
import * as fs from "node:fs";
import * as path from "node:path";
import * as os from "node:os";
interface ToolTimeout {
toolCallId: string;
@@ -28,6 +31,8 @@ interface ToolTimeout {
// Configuration
let timeoutSeconds = 30;
let enabled = true;
const SETTINGS_NAMESPACE = "slowtool";
const globalSettingsPath = path.join(os.homedir(), ".pi", "agent", "settings.json");
// Track running tools
const runningTools: Map<string, ToolTimeout> = new Map();
@@ -43,6 +48,55 @@ function formatDuration(ms: number): string {
return `${minutes}m ${remainingSeconds}s`;
}
function asRecord(value: unknown): Record<string, unknown> | undefined {
if (!value || typeof value !== "object") return undefined;
return value as Record<string, unknown>;
}
function readSettingsFile(filePath: string): Record<string, unknown> {
try {
if (!fs.existsSync(filePath)) return {};
const raw = fs.readFileSync(filePath, "utf-8");
const parsed = JSON.parse(raw) as unknown;
return asRecord(parsed) ?? {};
} catch {
return {};
}
}
function loadGlobalConfig(): { timeoutSeconds: number; enabled: boolean } {
const settings = readSettingsFile(globalSettingsPath);
const slowtoolSettings = asRecord(settings[SETTINGS_NAMESPACE]);
const configuredTimeout = slowtoolSettings?.timeoutSeconds;
const nextTimeout =
typeof configuredTimeout === "number" && Number.isFinite(configuredTimeout) && configuredTimeout >= 1
? Math.floor(configuredTimeout)
: 30;
const configuredEnabled = slowtoolSettings?.enabled;
const nextEnabled = typeof configuredEnabled === "boolean" ? configuredEnabled : true;
return { timeoutSeconds: nextTimeout, enabled: nextEnabled };
}
function saveGlobalConfig(next: { timeoutSeconds: number; enabled: boolean }): boolean {
try {
const settings = readSettingsFile(globalSettingsPath);
const existing = asRecord(settings[SETTINGS_NAMESPACE]) ?? {};
settings[SETTINGS_NAMESPACE] = {
...existing,
timeoutSeconds: next.timeoutSeconds,
enabled: next.enabled,
};
fs.mkdirSync(path.dirname(globalSettingsPath), { recursive: true });
fs.writeFileSync(globalSettingsPath, `${JSON.stringify(settings, null, 2)}\n`, "utf-8");
return true;
} catch {
return false;
}
}
function getCommandPreview(args: unknown): string | undefined {
if (!args) return undefined;
const anyArgs = args as Record<string, unknown>;
@@ -77,6 +131,29 @@ function notifyTimeout(pi: ExtensionAPI, tool: ToolTimeout): void {
// ============ EVENT HANDLERS ============
export default function(pi: ExtensionAPI) {
const applyPersistedConfig = () => {
const persisted = loadGlobalConfig();
timeoutSeconds = persisted.timeoutSeconds;
enabled = persisted.enabled;
};
const persistCurrentConfig = (ctx: ExtensionCommandContext): void => {
const ok = saveGlobalConfig({ timeoutSeconds, enabled });
if (!ok) {
ctx.ui.notify("Failed to persist slowtool settings", "warning");
}
};
applyPersistedConfig();
pi.on("session_start", async (_event, _ctx) => {
applyPersistedConfig();
});
pi.on("session_switch", async (_event, _ctx) => {
applyPersistedConfig();
});
// Register commands
pi.registerCommand("slowtool:timeout", {
description: "Set timeout threshold in seconds (default: 30)",
@@ -91,6 +168,7 @@ export default function(pi: ExtensionAPI) {
return;
}
timeoutSeconds = newTimeout;
persistCurrentConfig(ctx);
ctx.ui.notify(`Timeout set to ${timeoutSeconds}s`, "info");
},
});
@@ -99,6 +177,7 @@ export default function(pi: ExtensionAPI) {
description: "Enable slow tool notifications",
handler: async (_args: string, ctx: ExtensionCommandContext) => {
enabled = true;
persistCurrentConfig(ctx);
ctx.ui.notify("Slow tool notifications enabled", "info");
},
});
@@ -107,6 +186,7 @@ export default function(pi: ExtensionAPI) {
description: "Disable slow tool notifications",
handler: async (_args: string, ctx: ExtensionCommandContext) => {
enabled = false;
persistCurrentConfig(ctx);
ctx.ui.notify("Slow tool notifications disabled", "info");
},
});
@@ -0,0 +1,99 @@
---
name: jj-issue-workspaces
description: Create one Jujutsu workspace per issue, base them on an updated mainline bookmark like master, optionally create feature bookmarks, and open a zellij tab running pi in each workspace. Use when the user wants to fan out work across multiple issues, especially from a screenshot, Linear board, or issue list.
---
# JJ Issue Workspaces
This skill sets up a parallel issue workflow with `jj workspaces`.
Use it when the user wants any of the following:
- one workspace per issue
- multiple issues opened side by side
- a zellij tab for each issue
- `pi` opened in each issue workspace with a task-specific prompt
- issue fan-out from a screenshot, Linear board, or manually listed issues
## Workflow
1. Confirm the target repo and verify it is a `jj` repo.
2. If the user gave a screenshot path, use the `read` tool on the screenshot first and extract the issue keys and titles.
3. Decide the base bookmark/revision, usually `master` or `main`.
4. Run the helper script to:
- fetch the base bookmark from `origin`
- create sibling workspaces like `../Phoenix-spa-748`
- create bookmarks like `feature/spa-748`
- optionally open one zellij tab per workspace and launch `pi`
5. Tell the user which workspaces and tabs were created.
## Helper script
Use the helper script in this skill:
```bash
./scripts/jj-workspace-fanout.sh --help
```
Run it from anywhere. Pass absolute paths when convenient.
## Common usage
### Create workspaces and bookmarks only
```bash
./scripts/jj-workspace-fanout.sh \
--repo /path/to/repo \
--base master \
--issue "SPA-748=Wrap text in credits line items" \
--issue "SPA-428=Implement \"Downgrade\" Mimir modal (maximalist)" \
--issue "SPA-754=Resize seat count picker"
```
### Create workspaces, bookmarks, zellij tabs, and launch pi
```bash
./scripts/jj-workspace-fanout.sh \
--repo /path/to/repo \
--base master \
--session attio \
--open-pi \
--issue "SPA-748=Wrap text in credits line items" \
--issue "SPA-428=Implement \"Downgrade\" Mimir modal (maximalist)" \
--issue "SPA-754=Resize seat count picker"
```
### Recreate existing workspaces from scratch
```bash
./scripts/jj-workspace-fanout.sh \
--repo /path/to/repo \
--base master \
--session attio \
--open-pi \
--reset-existing \
--issue "SPA-748=Wrap text in credits line items"
```
## Defaults and conventions
- Workspace names use the lowercased issue key, for example `spa-748`
- Workspace directories are created beside the repo, for example `../Phoenix-spa-748`
- Bookmark names default to `feature/<issue-key-lowercase>`
- Base revision defaults to `master`
- Remote defaults to `origin`
- If `--open-pi` is used, the script launches `pi` in each workspace with a task-specific prompt
## Recommended agent behavior
When using this skill:
- Prefer `jj` over `git`
- Check `jj workspace list` before changing anything
- If the user says to update `master` or `main` first, let the script fetch that base revision before creating workspaces
- If the user wants an existing set recreated, use `--reset-existing`
- If zellij tabs already exist and the user wants a clean retry, close those tabs first or recreate the session
## Notes
- The script does not delete existing workspaces unless `--reset-existing` is provided.
- `--open-pi` requires a zellij session name, either via `--session <name>` or `ZELLIJ_SESSION_NAME`.
- If the repo uses `main` instead of `master`, pass `--base main`.
@@ -0,0 +1,292 @@
#!/usr/bin/env bash
set -euo pipefail
usage() {
cat <<'EOF'
Create one jj workspace per issue, optionally create bookmarks, and optionally open zellij tabs running pi.
Usage:
jj-workspace-fanout.sh [options] --issue "KEY=Title" [--issue "KEY=Title" ...]
Options:
--repo PATH Repo root (default: current directory)
--base REV Base revision/bookmark (default: master)
--remote NAME Git remote to fetch from (default: origin)
--issue KEY=TITLE Issue key and title (repeatable)
--session NAME Zellij session name (defaults to ZELLIJ_SESSION_NAME if set)
--open-pi Open a zellij tab per workspace and launch pi
--no-fetch Skip jj git fetch
--no-bookmarks Do not create feature/<issue> bookmarks
--keep-existing Skip creation for existing workspaces instead of failing
--reset-existing Forget and delete existing workspaces before recreating them
--prompt-suffix TEXT Extra text appended to each pi prompt
--pi-cmd CMD pi command to launch (default: pi)
--dry-run Print planned actions without making changes
--help Show this help
Examples:
jj-workspace-fanout.sh \
--repo /path/to/Phoenix \
--base master \
--issue "SPA-748=Wrap text in credits line items" \
--issue "SPA-754=Resize seat count picker"
jj-workspace-fanout.sh \
--repo /path/to/Phoenix \
--base master \
--session attio \
--open-pi \
--issue "SPA-748=Wrap text in credits line items"
EOF
}
require_cmd() {
if ! command -v "$1" >/dev/null 2>&1; then
echo "error: missing required command: $1" >&2
exit 1
fi
}
shell_escape() {
printf '%q' "$1"
}
log() {
printf '[jj-issue-workspaces] %s\n' "$*"
}
run() {
if [[ "$DRY_RUN" -eq 1 ]]; then
printf '[dry-run] '
printf '%q ' "$@"
printf '\n'
else
"$@"
fi
}
workspace_exists() {
local workspace_name="$1"
jj -R "$REPO" workspace list | awk -F: '{print $1}' | grep -Fxq "$workspace_name"
}
bookmark_exists() {
local workspace_dir="$1"
local bookmark_name="$2"
jj -R "$workspace_dir" bookmark list "$bookmark_name" 2>/dev/null | grep -Eq "^${bookmark_name}:"
}
close_tab_if_exists() {
local session_name="$1"
local tab_name="$2"
local tabs
tabs=$(zellij --session "$session_name" action query-tab-names 2>/dev/null || true)
if printf '%s\n' "$tabs" | grep -Fxq "$tab_name"; then
log "closing existing zellij tab $tab_name"
run zellij --session "$session_name" action go-to-tab-name "$tab_name"
run zellij --session "$session_name" action close-tab
fi
}
launch_pi_tab() {
local session_name="$1"
local tab_name="$2"
local workspace_dir="$3"
local prompt="$4"
local cmd
cmd="cd $(shell_escape "$workspace_dir") && pwd && $PI_CMD $(shell_escape "$prompt")"
close_tab_if_exists "$session_name" "$tab_name"
run zellij --session "$session_name" action new-tab --name "$tab_name"
run zellij --session "$session_name" action write-chars "$cmd"
run zellij --session "$session_name" action write 10
}
REPO="$(pwd)"
BASE="master"
REMOTE="origin"
SESSION="${ZELLIJ_SESSION_NAME:-}"
OPEN_PI=0
FETCH=1
CREATE_BOOKMARKS=1
KEEP_EXISTING=0
RESET_EXISTING=0
DRY_RUN=0
PROMPT_SUFFIX=""
PI_CMD="pi"
declare -a ISSUES=()
while [[ $# -gt 0 ]]; do
case "$1" in
--repo)
REPO="$2"
shift 2
;;
--base)
BASE="$2"
shift 2
;;
--remote)
REMOTE="$2"
shift 2
;;
--issue)
ISSUES+=("$2")
shift 2
;;
--session)
SESSION="$2"
shift 2
;;
--open-pi)
OPEN_PI=1
shift
;;
--no-fetch)
FETCH=0
shift
;;
--no-bookmarks)
CREATE_BOOKMARKS=0
shift
;;
--keep-existing)
KEEP_EXISTING=1
shift
;;
--reset-existing)
RESET_EXISTING=1
shift
;;
--prompt-suffix)
PROMPT_SUFFIX="$2"
shift 2
;;
--pi-cmd)
PI_CMD="$2"
shift 2
;;
--dry-run)
DRY_RUN=1
shift
;;
--help|-h)
usage
exit 0
;;
*)
echo "error: unknown argument: $1" >&2
usage >&2
exit 1
;;
esac
done
if [[ ${#ISSUES[@]} -eq 0 ]]; then
echo "error: at least one --issue KEY=TITLE is required" >&2
exit 1
fi
if [[ "$KEEP_EXISTING" -eq 1 && "$RESET_EXISTING" -eq 1 ]]; then
echo "error: --keep-existing and --reset-existing cannot be combined" >&2
exit 1
fi
REPO="$(cd "$REPO" && pwd)"
PARENT_DIR="$(dirname "$REPO")"
REPO_BASENAME="$(basename "$REPO")"
require_cmd jj
if [[ "$OPEN_PI" -eq 1 ]]; then
require_cmd zellij
if [[ -z "$SESSION" ]]; then
echo "error: --open-pi requires --session <name> or ZELLIJ_SESSION_NAME" >&2
exit 1
fi
fi
if [[ ! -d "$REPO/.jj" ]]; then
echo "error: repo is not a jj repository: $REPO" >&2
exit 1
fi
if [[ "$FETCH" -eq 1 ]]; then
log "fetching $BASE from $REMOTE"
run jj -R "$REPO" git fetch --remote "$REMOTE" --branch "$BASE"
fi
log "validating base revision $BASE"
run jj -R "$REPO" log -r "$BASE" --no-pager
created_workspaces=()
for issue in "${ISSUES[@]}"; do
if [[ "$issue" != *=* ]]; then
echo "error: issue must be formatted as KEY=TITLE: $issue" >&2
exit 1
fi
issue_key="${issue%%=*}"
issue_title="${issue#*=}"
issue_slug="$(printf '%s' "$issue_key" | tr '[:upper:]' '[:lower:]')"
workspace_name="$issue_slug"
workspace_dir="$PARENT_DIR/${REPO_BASENAME}-${issue_slug}"
bookmark_name="feature/$issue_slug"
prompt="Work on ${issue_key}: ${issue_title}. You are in the dedicated jj workspace for this issue. First inspect the relevant code, identify the main components involved, and propose a short plan before editing."
if [[ -n "$PROMPT_SUFFIX" ]]; then
prompt+=" ${PROMPT_SUFFIX}"
fi
if workspace_exists "$workspace_name" || [[ -e "$workspace_dir" ]]; then
if [[ "$RESET_EXISTING" -eq 1 ]]; then
log "resetting existing workspace $workspace_name"
if workspace_exists "$workspace_name"; then
run jj -R "$REPO" workspace forget "$workspace_name"
fi
run rm -rf "$workspace_dir"
elif [[ "$KEEP_EXISTING" -eq 1 ]]; then
log "keeping existing workspace $workspace_name at $workspace_dir"
else
echo "error: workspace already exists: $workspace_name ($workspace_dir). Use --keep-existing or --reset-existing." >&2
exit 1
fi
fi
if ! workspace_exists "$workspace_name"; then
log "creating workspace $workspace_name at $workspace_dir"
run jj -R "$REPO" workspace add --name "$workspace_name" -r "$BASE" "$workspace_dir"
fi
if [[ "$CREATE_BOOKMARKS" -eq 1 ]]; then
log "ensuring bookmark $bookmark_name exists"
if bookmark_exists "$workspace_dir" "$bookmark_name"; then
run jj -R "$workspace_dir" bookmark set "$bookmark_name" -r @
else
run jj -R "$workspace_dir" bookmark create "$bookmark_name"
fi
fi
if [[ "$OPEN_PI" -eq 1 ]]; then
log "opening zellij tab $workspace_name in session $SESSION"
run launch_pi_tab "$SESSION" "$workspace_name" "$workspace_dir" "$prompt"
fi
created_workspaces+=("$workspace_name:$workspace_dir:$bookmark_name")
done
printf '\nCreated/updated workspaces:\n'
for item in "${created_workspaces[@]}"; do
IFS=':' read -r workspace_name workspace_dir bookmark_name <<<"$item"
printf ' - %s -> %s' "$workspace_name" "$workspace_dir"
if [[ "$CREATE_BOOKMARKS" -eq 1 ]]; then
printf ' [%s]' "$bookmark_name"
fi
printf '\n'
done
if [[ "$OPEN_PI" -eq 1 ]]; then
printf '\nZellij session: %s\n' "$SESSION"
fi
+102
View File
@@ -0,0 +1,102 @@
#!/usr/bin/env bash
set -euo pipefail
# Cleans up zellij sessions that are inactive:
# - sessions marked EXITED (resurrectable metadata)
# - running sessions with 0 attached clients
#
# Usage:
# cleanup-zellij-inactive.sh # delete inactive sessions
# cleanup-zellij-inactive.sh --dry-run # show what would be deleted
DRY_RUN=0
case "${1-}" in
"" ) ;;
-n|--dry-run) DRY_RUN=1 ;;
-h|--help)
cat <<'EOF'
cleanup-zellij-inactive.sh
Delete zellij sessions that are inactive:
- EXITED sessions are deleted
- running sessions with 0 attached clients are killed+deleted
Options:
-n, --dry-run Show what would be deleted
-h, --help Show this help
EOF
exit 0
;;
*)
echo "Unknown option: $1" >&2
echo "Use --help for usage" >&2
exit 1
;;
esac
if ! command -v zellij >/dev/null 2>&1; then
echo "zellij not found in PATH" >&2
exit 1
fi
mapfile -t session_lines < <(zellij list-sessions --no-formatting 2>/dev/null || true)
if [ "${#session_lines[@]}" -eq 0 ]; then
echo "No zellij sessions found"
exit 0
fi
deleted=0
failed=0
kept=0
for line in "${session_lines[@]}"; do
[ -z "$line" ] && continue
name="${line%% *}"
is_exited=0
if [[ "$line" == *"EXITED"* ]]; then
is_exited=1
fi
should_delete=0
if [ "$is_exited" -eq 1 ]; then
should_delete=1
else
# Running session: check attached clients
clients_out="$(zellij --session "$name" action list-clients 2>/dev/null || true)"
client_count="$(printf '%s\n' "$clients_out" | tail -n +2 | sed '/^\s*$/d' | wc -l | tr -d ' ')"
if [ "$client_count" -eq 0 ]; then
should_delete=1
fi
fi
if [ "$should_delete" -eq 1 ]; then
if [ "$DRY_RUN" -eq 1 ]; then
echo "[dry-run] delete: $name"
deleted=$((deleted + 1))
else
# --force also kills running sessions before deleting
if zellij delete-session --force "$name" >/dev/null 2>&1; then
echo "deleted: $name"
deleted=$((deleted + 1))
else
echo "failed: $name" >&2
failed=$((failed + 1))
fi
fi
else
kept=$((kept + 1))
fi
done
echo
if [ "$DRY_RUN" -eq 1 ]; then
echo "Would delete: $deleted"
else
echo "Deleted: $deleted"
echo "Failed: $failed"
fi
echo "Kept: $kept"
+1 -1
View File
@@ -328,7 +328,7 @@ default_layout "compact"
// The folder in which Zellij will look for themes
// (Requires restart)
//
// theme_dir "/tmp"
// theme_dir "/home/thomasgl/.config/zellij/themes"
// Toggle enabling the mouse mode.
// On certain configurations, or terminals this could