Compare commits
13 Commits
nixos
...
534ec8b99f
| Author | SHA1 | Date | |
|---|---|---|---|
| 534ec8b99f | |||
| c004356b5a | |||
| 8fa80f58ea | |||
| b42a9ecffa | |||
| 966e40e71b | |||
| 4af7031922 | |||
| 6003f41a12 | |||
| 587c54060b | |||
| cad0540600 | |||
| fd2307eb0c | |||
| 6d525d0971 | |||
| 51073c07a8 | |||
| e4b6fbabc6 |
@@ -194,6 +194,17 @@
|
|||||||
"centeringMode": "index",
|
"centeringMode": "index",
|
||||||
"clockDateFormat": "d MMM yyyy",
|
"clockDateFormat": "d MMM yyyy",
|
||||||
"lockDateFormat": "",
|
"lockDateFormat": "",
|
||||||
|
"greeterRememberLastSession": true,
|
||||||
|
"greeterRememberLastUser": true,
|
||||||
|
"greeterEnableFprint": false,
|
||||||
|
"greeterEnableU2f": false,
|
||||||
|
"greeterWallpaperPath": "",
|
||||||
|
"greeterUse24HourClock": true,
|
||||||
|
"greeterShowSeconds": false,
|
||||||
|
"greeterPadHours12Hour": false,
|
||||||
|
"greeterLockDateFormat": "",
|
||||||
|
"greeterFontFamily": "",
|
||||||
|
"greeterWallpaperFillMode": "",
|
||||||
"mediaSize": 1,
|
"mediaSize": 1,
|
||||||
"appLauncherViewMode": "list",
|
"appLauncherViewMode": "list",
|
||||||
"spotlightModalViewMode": "list",
|
"spotlightModalViewMode": "list",
|
||||||
@@ -314,6 +325,7 @@
|
|||||||
"matugenTemplateKcolorscheme": true,
|
"matugenTemplateKcolorscheme": true,
|
||||||
"matugenTemplateVscode": true,
|
"matugenTemplateVscode": true,
|
||||||
"matugenTemplateEmacs": true,
|
"matugenTemplateEmacs": true,
|
||||||
|
"matugenTemplateZed": true,
|
||||||
"showDock": false,
|
"showDock": false,
|
||||||
"dockAutoHide": false,
|
"dockAutoHide": false,
|
||||||
"dockSmartAutoHide": false,
|
"dockSmartAutoHide": false,
|
||||||
@@ -355,6 +367,8 @@
|
|||||||
"lockAtStartup": false,
|
"lockAtStartup": false,
|
||||||
"enableFprint": false,
|
"enableFprint": false,
|
||||||
"maxFprintTries": 3,
|
"maxFprintTries": 3,
|
||||||
|
"enableU2f": false,
|
||||||
|
"u2fMode": "or",
|
||||||
"lockScreenActiveMonitor": "all",
|
"lockScreenActiveMonitor": "all",
|
||||||
"lockScreenInactiveColor": "#000000",
|
"lockScreenInactiveColor": "#000000",
|
||||||
"lockScreenNotificationMode": 0,
|
"lockScreenNotificationMode": 0,
|
||||||
|
|||||||
@@ -1,3 +1,3 @@
|
|||||||
if test (uname) = Darwin
|
if test (uname) = Darwin
|
||||||
fnm env --use-on-cd --shell fish | source
|
fnm env --use-on-cd --log-level=quiet --shell fish | source
|
||||||
end
|
end
|
||||||
|
|||||||
+10
-1
@@ -102,9 +102,18 @@ status is-interactive; and begin
|
|||||||
|
|
||||||
end
|
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"
|
if test -d "$HOME/.local/bin"
|
||||||
|
if test (uname) = Linux
|
||||||
|
fish_add_path -a -m "$HOME/.local/bin"
|
||||||
|
else
|
||||||
fish_add_path "$HOME/.local/bin"
|
fish_add_path "$HOME/.local/bin"
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
# pnpm
|
# pnpm
|
||||||
|
|||||||
@@ -1,8 +1,11 @@
|
|||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
|
set -u
|
||||||
|
|
||||||
screenshot_dir="$HOME/Pictures/Screenshots"
|
screenshot_dir="$HOME/Pictures/Screenshots"
|
||||||
remote_target="mac-attio:~/screenshot.png"
|
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() {
|
notify() {
|
||||||
DBUS_SESSION_BUS_ADDRESS="unix:path=/run/user/$(id -u)/bus" \
|
DBUS_SESSION_BUS_ADDRESS="unix:path=/run/user/$(id -u)/bus" \
|
||||||
@@ -15,12 +18,13 @@ shopt -s nullglob
|
|||||||
existing_files=("$screenshot_dir"/*.png)
|
existing_files=("$screenshot_dir"/*.png)
|
||||||
existing_count=${#existing_files[@]}
|
existing_count=${#existing_files[@]}
|
||||||
|
|
||||||
# Take screenshot
|
# Take screenshot (no timeout here so interactive capture isn't canceled)
|
||||||
niri msg action screenshot
|
niri msg action screenshot >/dev/null 2>&1
|
||||||
|
|
||||||
# Wait for new file (timeout in 0.1s intervals)
|
# Wait for new file (timeout in 0.1s intervals)
|
||||||
deadline=$((timeout * 10))
|
deadline=$((file_timeout * 10))
|
||||||
count=0
|
count=0
|
||||||
|
files=("$screenshot_dir"/*.png)
|
||||||
|
|
||||||
while (( count < deadline )); do
|
while (( count < deadline )); do
|
||||||
files=("$screenshot_dir"/*.png)
|
files=("$screenshot_dir"/*.png)
|
||||||
@@ -37,12 +41,20 @@ if (( ${#files[@]} <= existing_count )); then
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
# Get the new file (most recent)
|
# 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
|
# Small delay to ensure file is fully written
|
||||||
sleep 0.1
|
sleep 0.1
|
||||||
|
|
||||||
# Upload
|
# Upload with strict SSH options so it never blocks waiting for prompts
|
||||||
if scp -q "$latest_file" "$remote_target"; then
|
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"
|
notify "Screenshot" "Uploaded to Mac"
|
||||||
|
else
|
||||||
|
notify "Screenshot" "Upload to Mac failed"
|
||||||
fi
|
fi
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
return {
|
return {
|
||||||
cmd = { "biome", "lsp-proxy" },
|
cmd = { "npx", "biome", "lsp-proxy" },
|
||||||
filetypes = {
|
filetypes = {
|
||||||
"javascript",
|
"javascript",
|
||||||
"javascriptreact",
|
"javascriptreact",
|
||||||
|
|||||||
@@ -32,6 +32,23 @@ return {
|
|||||||
},
|
},
|
||||||
"folke/lazydev.nvim",
|
"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'
|
--- @module 'blink.cmp'
|
||||||
--- @type blink.cmp.Config
|
--- @type blink.cmp.Config
|
||||||
opts = {
|
opts = {
|
||||||
@@ -76,9 +93,16 @@ return {
|
|||||||
},
|
},
|
||||||
|
|
||||||
sources = {
|
sources = {
|
||||||
default = { "lsp", "path", "snippets", "lazydev" },
|
default = { "lsp", "path", "snippets", "lazydev", "minuet" },
|
||||||
providers = {
|
providers = {
|
||||||
lazydev = { module = "lazydev.integrations.blink", score_offset = 100 },
|
lazydev = { module = "lazydev.integrations.blink", score_offset = 100 },
|
||||||
|
minuet = {
|
||||||
|
name = "minuet",
|
||||||
|
module = "minuet.blink",
|
||||||
|
async = true,
|
||||||
|
timeout_ms = 3000,
|
||||||
|
score_offset = 50,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|||||||
@@ -6,12 +6,12 @@ return {
|
|||||||
-- Allows extra capabilities provided by blink.cmp
|
-- Allows extra capabilities provided by blink.cmp
|
||||||
{
|
{
|
||||||
"saghen/blink.cmp",
|
"saghen/blink.cmp",
|
||||||
config = function(_, opts)
|
opts = function(_, opts)
|
||||||
require("blink.cmp").setup(opts)
|
|
||||||
-- Add blink.cmp capabilities to the default LSP client capabilities
|
-- Add blink.cmp capabilities to the default LSP client capabilities
|
||||||
vim.lsp.config("*", {
|
vim.lsp.config("*", {
|
||||||
capabilities = require("blink.cmp").get_lsp_capabilities(),
|
capabilities = require("blink.cmp").get_lsp_capabilities(),
|
||||||
})
|
})
|
||||||
|
return opts
|
||||||
end,
|
end,
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|||||||
@@ -28,34 +28,16 @@ return {
|
|||||||
end,
|
end,
|
||||||
},
|
},
|
||||||
{ "nvim-lua/plenary.nvim" },
|
{ "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",
|
"saghen/blink.cmp",
|
||||||
config = function()
|
opts = function(_, opts)
|
||||||
require("blink-cmp").setup({
|
opts.keymap = opts.keymap or {}
|
||||||
keymap = {
|
opts.keymap["<A-y>"] = require("minuet").make_blink_map()
|
||||||
-- Manually invoke minuet completion.
|
opts.completion = opts.completion or {}
|
||||||
["<A-y>"] = require("minuet").make_blink_map(),
|
opts.completion.trigger = opts.completion.trigger or {}
|
||||||
},
|
opts.completion.trigger.prefetch_on_insert = false
|
||||||
sources = {
|
return opts
|
||||||
-- 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 } },
|
|
||||||
})
|
|
||||||
end,
|
end,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"activePack": "glados",
|
"activePack": "glados",
|
||||||
"volume": 1,
|
"volume": 1,
|
||||||
"muted": false,
|
"muted": true,
|
||||||
"enabledCategories": {
|
"enabledCategories": {
|
||||||
"session.start": true,
|
"session.start": true,
|
||||||
"task.acknowledge": true,
|
"task.acknowledge": true,
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"activePack": "solid_snake",
|
"activePack": "solid_snake",
|
||||||
"volume": 1.5,
|
"volume": 1.5,
|
||||||
"muted": false,
|
"muted": true,
|
||||||
"enabledCategories": {
|
"enabledCategories": {
|
||||||
"session.start": true,
|
"session.start": true,
|
||||||
"task.acknowledge": true,
|
"task.acknowledge": true,
|
||||||
|
|||||||
@@ -1,11 +1,15 @@
|
|||||||
{
|
{
|
||||||
"lastChangelogVersion": "0.63.1",
|
"lastChangelogVersion": "0.67.3",
|
||||||
"defaultProvider": "openai-codex",
|
"defaultProvider": "cursor-acp",
|
||||||
"defaultModel": "gpt-5.3-codex",
|
"defaultModel": "auto",
|
||||||
"defaultThinkingLevel": "high",
|
"defaultThinkingLevel": "medium",
|
||||||
"theme": "matugen",
|
"theme": "matugen",
|
||||||
"lsp": {
|
"lsp": {
|
||||||
"hookMode": "edit_write"
|
"hookMode": "edit_write"
|
||||||
},
|
},
|
||||||
"hideThinkingBlock": false
|
"hideThinkingBlock": false,
|
||||||
|
"slowtool": {
|
||||||
|
"timeoutSeconds": 300,
|
||||||
|
"enabled": true
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -1,11 +1,15 @@
|
|||||||
{
|
{
|
||||||
"lastChangelogVersion": "0.57.1",
|
"lastChangelogVersion": "0.66.1",
|
||||||
"defaultProvider": "anthropic",
|
"defaultProvider": "openai-codex",
|
||||||
"defaultModel": "claude-opus-4-6",
|
"defaultModel": "gpt-5.4",
|
||||||
"defaultThinkingLevel": "medium",
|
"defaultThinkingLevel": "medium",
|
||||||
"theme": "matugen",
|
"theme": "matugen",
|
||||||
"lsp": {
|
"lsp": {
|
||||||
"hookMode": "edit_write"
|
"hookMode": "edit_write"
|
||||||
},
|
},
|
||||||
"hideThinkingBlock": false
|
"hideThinkingBlock": true,
|
||||||
|
"slowtool": {
|
||||||
|
"timeoutSeconds": 300,
|
||||||
|
"enabled": true
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -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
|
# 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.
|
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
@@ -308,9 +308,24 @@ function pickSound(categoryConfig: CategoryConfig, category: Category): Sound |
|
|||||||
|
|
||||||
// ============ SOUND PLAYBACK ============
|
// ============ SOUND PLAYBACK ============
|
||||||
function play(category: Category): void {
|
function play(category: Category): void {
|
||||||
if (config.muted) return;
|
|
||||||
if (!config.enabledCategories[category]) 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();
|
const now = Date.now();
|
||||||
if (now - lastPlayed < DEBOUNCE_MS) {
|
if (now - lastPlayed < DEBOUNCE_MS) {
|
||||||
return;
|
return;
|
||||||
@@ -345,20 +360,6 @@ function play(category: Category): void {
|
|||||||
}
|
}
|
||||||
|
|
||||||
playSound(soundPath, config.volume);
|
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 ============
|
// ============ COMMANDS ============
|
||||||
@@ -814,7 +815,7 @@ async function showTestMenu(ctx: ExtensionCommandContext) {
|
|||||||
|
|
||||||
const INTERACTIVE_TOOLS = new Set(["question", "questionnaire"]);
|
const INTERACTIVE_TOOLS = new Set(["question", "questionnaire"]);
|
||||||
|
|
||||||
export default function (pi: ExtensionAPI) {
|
export default function(pi: ExtensionAPI) {
|
||||||
registerCommands(pi);
|
registerCommands(pi);
|
||||||
|
|
||||||
pi.on("session_start", async (_event, ctx) => {
|
pi.on("session_start", async (_event, ctx) => {
|
||||||
|
|||||||
@@ -14,6 +14,9 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import type { ExtensionAPI, ExtensionCommandContext } from "@mariozechner/pi-coding-agent";
|
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 {
|
interface ToolTimeout {
|
||||||
toolCallId: string;
|
toolCallId: string;
|
||||||
@@ -28,6 +31,8 @@ interface ToolTimeout {
|
|||||||
// Configuration
|
// Configuration
|
||||||
let timeoutSeconds = 30;
|
let timeoutSeconds = 30;
|
||||||
let enabled = true;
|
let enabled = true;
|
||||||
|
const SETTINGS_NAMESPACE = "slowtool";
|
||||||
|
const globalSettingsPath = path.join(os.homedir(), ".pi", "agent", "settings.json");
|
||||||
|
|
||||||
// Track running tools
|
// Track running tools
|
||||||
const runningTools: Map<string, ToolTimeout> = new Map();
|
const runningTools: Map<string, ToolTimeout> = new Map();
|
||||||
@@ -43,6 +48,55 @@ function formatDuration(ms: number): string {
|
|||||||
return `${minutes}m ${remainingSeconds}s`;
|
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 {
|
function getCommandPreview(args: unknown): string | undefined {
|
||||||
if (!args) return undefined;
|
if (!args) return undefined;
|
||||||
const anyArgs = args as Record<string, unknown>;
|
const anyArgs = args as Record<string, unknown>;
|
||||||
@@ -77,6 +131,29 @@ function notifyTimeout(pi: ExtensionAPI, tool: ToolTimeout): void {
|
|||||||
// ============ EVENT HANDLERS ============
|
// ============ EVENT HANDLERS ============
|
||||||
|
|
||||||
export default function(pi: ExtensionAPI) {
|
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
|
// Register commands
|
||||||
pi.registerCommand("slowtool:timeout", {
|
pi.registerCommand("slowtool:timeout", {
|
||||||
description: "Set timeout threshold in seconds (default: 30)",
|
description: "Set timeout threshold in seconds (default: 30)",
|
||||||
@@ -91,6 +168,7 @@ export default function(pi: ExtensionAPI) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
timeoutSeconds = newTimeout;
|
timeoutSeconds = newTimeout;
|
||||||
|
persistCurrentConfig(ctx);
|
||||||
ctx.ui.notify(`Timeout set to ${timeoutSeconds}s`, "info");
|
ctx.ui.notify(`Timeout set to ${timeoutSeconds}s`, "info");
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
@@ -99,6 +177,7 @@ export default function(pi: ExtensionAPI) {
|
|||||||
description: "Enable slow tool notifications",
|
description: "Enable slow tool notifications",
|
||||||
handler: async (_args: string, ctx: ExtensionCommandContext) => {
|
handler: async (_args: string, ctx: ExtensionCommandContext) => {
|
||||||
enabled = true;
|
enabled = true;
|
||||||
|
persistCurrentConfig(ctx);
|
||||||
ctx.ui.notify("Slow tool notifications enabled", "info");
|
ctx.ui.notify("Slow tool notifications enabled", "info");
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
@@ -107,6 +186,7 @@ export default function(pi: ExtensionAPI) {
|
|||||||
description: "Disable slow tool notifications",
|
description: "Disable slow tool notifications",
|
||||||
handler: async (_args: string, ctx: ExtensionCommandContext) => {
|
handler: async (_args: string, ctx: ExtensionCommandContext) => {
|
||||||
enabled = false;
|
enabled = false;
|
||||||
|
persistCurrentConfig(ctx);
|
||||||
ctx.ui.notify("Slow tool notifications disabled", "info");
|
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
|
||||||
Executable
+102
@@ -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"
|
||||||
@@ -328,7 +328,7 @@ default_layout "compact"
|
|||||||
// The folder in which Zellij will look for themes
|
// The folder in which Zellij will look for themes
|
||||||
// (Requires restart)
|
// (Requires restart)
|
||||||
//
|
//
|
||||||
// theme_dir "/tmp"
|
// theme_dir "/home/thomasgl/.config/zellij/themes"
|
||||||
|
|
||||||
// Toggle enabling the mouse mode.
|
// Toggle enabling the mouse mode.
|
||||||
// On certain configurations, or terminals this could
|
// On certain configurations, or terminals this could
|
||||||
|
|||||||
Reference in New Issue
Block a user