feat(sf): Tier 2 — SHOW_FILE tool, STATUS_LINE runner, /keep-alive, /sidekicks, Ctrl+G/T/X keybindings

sf-tui/index.js:
- Import getExperimentalFlag / setExperimentalFlag from experimental.js
- Ctrl+G — open project root in $EDITOR
- Ctrl+T — toggle show_reasoning experimental flag
- Ctrl+Alt+B — open /tasks background surface
- Ctrl+Alt+O — open last URL from agent output in browser
- STATUS_LINE runner: setInterval 5s, execFile user script, pipe stdout to ctx.ui.setStatus
- SHOW_FILE tool: pi.registerTool({name:'show_file',...}) gated on show_file flag; reads file slice, renders as fenced code block

handlers/ops.js:
- /keep-alive [off] — spawns caffeinate (macOS) or systemd-inhibit (Linux) as detached process; /keep-alive off kills it

handlers/core.js:
- /sidekicks — reads .sf/parallel/ subdirs, shows STATUS per worker

catalog.js:
- Add /sidekicks and /keep-alive to TOP_LEVEL_SUBCOMMANDS

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
Mikael Hugo 2026-05-09 05:33:24 +02:00
parent eaf7165893
commit c1c3195f75
4 changed files with 281 additions and 0 deletions

View file

@ -12,6 +12,10 @@ import { Key } from "@singularity-forge/pi-tui";
import { getAutoSession } from "../sf/auto/session.js";
import { isAutoActive } from "../sf/auto.js";
import { projectRoot } from "../sf/commands/context.js";
import {
getExperimentalFlag,
setExperimentalFlag,
} from "../sf/experimental.js";
import { registerSessionColor } from "./color-band.js";
import { registerSessionEmoji } from "./emoji.js";
import { renderAutoFooter, renderFooter } from "./footer.js";
@ -169,6 +173,74 @@ export default function sfTui(pi) {
"Cycle permission profile (restricted→normal→trusted→unrestricted)",
handler: () => cyclePermissionProfile(ctx),
});
// Ctrl+G — open current project in $EDITOR (or notify if none)
pi.registerShortcut(Key.ctrl("g"), {
description: "Open project root in $EDITOR",
handler: () => {
const editor = process.env.EDITOR || process.env.VISUAL;
if (!editor) {
ctx.ui.notify(
"No $EDITOR set. Set EDITOR=code (or vim, nano, etc.) in your shell.",
"warning",
);
return;
}
const { spawn } = require("node:child_process");
spawn(editor, [projectRoot() ?? "."], {
stdio: "ignore",
detached: true,
}).unref();
ctx.ui.notify(`Opened ${editor} ${projectRoot() ?? "."}`, "info");
},
});
// Ctrl+T — toggle reasoning display
pi.registerShortcut(Key.ctrl("t"), {
description: "Toggle extended thinking / reasoning display",
handler: () => {
const current = getExperimentalFlag("show_reasoning");
setExperimentalFlag("show_reasoning", !current);
ctx.ui.notify(`Reasoning display ${current ? "OFF" : "ON"}`, "info");
},
});
// Ctrl+X B — open background tasks surface (/tasks)
pi.registerShortcut(Key.ctrlAlt("b"), {
description: "Open background tasks surface",
handler: () => {
ctx.sendMessage?.("/tasks");
},
});
// Ctrl+X O — open URL from last agent message
pi.registerShortcut(Key.ctrlAlt("o"), {
description: "Open last URL from agent output in browser",
handler: () => {
const { execSync } = require("node:child_process");
const os = require("node:os");
const entries = ctx.sessionManager?.getEntries?.() ?? [];
const lastText =
[...entries].reverse().find((e) => e.type === "assistant")?.content ??
"";
const urlMatch = String(lastText).match(/https?:\/\/[^\s"'<>)]+/);
if (!urlMatch) {
ctx.ui.notify("No URL found in last agent output.", "info");
return;
}
const url = urlMatch[0];
try {
const openCmd =
os.platform() === "darwin"
? "open"
: os.platform() === "win32"
? "start"
: "xdg-open";
execSync(`${openCmd} "${url}"`, { stdio: "ignore" });
ctx.ui.notify(`Opened: ${url}`, "info");
} catch {
ctx.ui.notify(`URL: ${url}`, "info");
}
},
});
// STATUS_LINE — spawn user script every 5s, pipe stdout to footer chip
startStatusLineRunner(ctx);
wasAutoActive = isAutoActive();
});
pi.on("before_agent_start", async (event) => {
@ -205,4 +277,83 @@ export default function sfTui(pi) {
}
wasAutoActive = autoNow;
});
// SHOW_FILE tool — renders a file path + optional line range as a code block
// in the agent timeline when the experimental flag is enabled.
pi.registerTool({
name: "show_file",
description:
"Display a file (or a section of it) as a highlighted code block in the conversation timeline. Use when you want to show the user specific code without just dumping text.",
parameters: {
type: "object",
properties: {
path: {
type: "string",
description: "Absolute or relative path to the file",
},
start_line: {
type: "number",
description: "First line to display (1-indexed, optional)",
},
end_line: {
type: "number",
description: "Last line to display (inclusive, optional)",
},
},
required: ["path"],
},
execute: async ({ path: filePath, start_line, end_line }) => {
if (!getExperimentalFlag("show_file")) {
return {
output:
"SHOW_FILE is not enabled. Run /experimental on show_file to enable.",
};
}
const { readFileSync, existsSync } = await import("node:fs");
const { resolve } = await import("node:path");
const abs = resolve(projectRoot() ?? ".", filePath);
if (!existsSync(abs)) {
return { output: `File not found: ${filePath}` };
}
const raw = readFileSync(abs, "utf-8");
const lines = raw.split("\n");
const from = start_line != null ? Math.max(1, start_line) - 1 : 0;
const to =
end_line != null ? Math.min(lines.length, end_line) : lines.length;
const slice = lines.slice(from, to).join("\n");
const ext = abs.split(".").pop() ?? "";
return { output: `\`\`\`${ext}\n${slice}\n\`\`\`` };
},
renderResult: ({ output }) => output,
});
}
/** Run the STATUS_LINE user script on a 5s interval, posting stdout to footer. */
let _statusLineInterval = null;
function startStatusLineRunner(ctx) {
if (_statusLineInterval) {
clearInterval(_statusLineInterval);
_statusLineInterval = null;
}
const tick = async () => {
if (!getExperimentalFlag("status_line")) return;
const scriptPath = getExperimentalFlag("status_line_script");
if (!scriptPath || typeof scriptPath !== "string") return;
try {
const { execFile } = await import("node:child_process");
const { promisify } = await import("node:util");
const execFileAsync = promisify(execFile);
const { stdout } = await execFileAsync(scriptPath, [], {
timeout: 4000,
encoding: "utf-8",
});
const text = stdout.trim().slice(0, 120);
if (text) {
ctx.ui.setStatus?.("sf-status-line", text);
}
} catch {
/* Non-fatal — script may be missing or timing out */
}
};
_statusLineInterval = setInterval(tick, 5000);
tick();
}

View file

@ -277,6 +277,14 @@ export const TOP_LEVEL_SUBCOMMANDS = [
cmd: "instructions",
desc: "List instruction files loaded into the agent context",
},
{
cmd: "sidekicks",
desc: "List background parallel worker jobs and their status",
},
{
cmd: "keep-alive",
desc: "Prevent system sleep during long runs (caffeinate / systemd-inhibit)",
},
];
export const DIRECT_SF_COMMANDS = TOP_LEVEL_SUBCOMMANDS.filter(

View file

@ -849,6 +849,10 @@ export async function handleCoreCommand(trimmed, ctx, pi) {
);
return true;
}
if (trimmed === "sidekicks" || trimmed.startsWith("sidekicks ")) {
await handleSidekicksCommand(ctx);
return true;
}
return false;
}
@ -1288,3 +1292,48 @@ export function formatTextStatus(state) {
}
return lines.join("\n");
}
// ─── /sidekicks ──────────────────────────────────────────────────────────────
async function handleSidekicksCommand(ctx) {
try {
const { readdirSync, readFileSync, existsSync } = await import("node:fs");
const { join: pathJoin } = await import("node:path");
const root = projectRoot();
const parallelDir = pathJoin(root, ".sf", "parallel");
if (!existsSync(parallelDir)) {
ctx.ui.notify(
"No background workers running. Start with /parallel start.",
"info",
);
return;
}
const entries = readdirSync(parallelDir, { withFileTypes: true })
.filter((d) => d.isDirectory())
.map((d) => d.name);
if (entries.length === 0) {
ctx.ui.notify("No sidekick workers found.", "info");
return;
}
const lines = [`Sidekicks (${entries.length})\n`];
for (const name of entries) {
const statusPath = pathJoin(parallelDir, name, "STATUS");
const status = existsSync(statusPath)
? readFileSync(statusPath, "utf-8").trim()
: "unknown";
const icon =
status === "running"
? "▶"
: status === "done"
? "✓"
: status === "error"
? "✗"
: "○";
lines.push(` ${icon} ${name.padEnd(16)} ${status}`);
}
lines.push("\nUse /parallel status for details.");
ctx.ui.notify(lines.join("\n"), "info");
} catch (e) {
ctx.ui.notify(`Sidekicks error: ${e.message}`, "error");
}
}

View file

@ -495,5 +495,78 @@ Examples:
ctx.ui.notify("Usage: /plan promote|list|diff|specs ...", "info");
return true;
}
if (trimmed === "keep-alive" || trimmed.startsWith("keep-alive ")) {
await handleKeepAlive(trimmed.replace(/^keep-alive\s*/, "").trim(), ctx);
return true;
}
return false;
}
// ─── /keep-alive ─────────────────────────────────────────────────────────────
let _keepAliveProcess = null;
async function handleKeepAlive(args, ctx) {
const os = await import("node:os");
const { spawn } = await import("node:child_process");
if (args === "off" || args === "stop") {
if (_keepAliveProcess) {
_keepAliveProcess.kill();
_keepAliveProcess = null;
ctx.ui.notify("Keep-alive stopped.", "info");
} else {
ctx.ui.notify("Keep-alive is not running.", "info");
}
return;
}
if (_keepAliveProcess) {
ctx.ui.notify(
"Keep-alive is already running. Use /keep-alive off to stop it.",
"info",
);
return;
}
const platform = os.platform();
let cmd, cmdArgs;
if (platform === "darwin") {
cmd = "caffeinate";
cmdArgs = ["-dims"];
} else if (platform === "linux") {
// systemd-inhibit keeps the session alive (no-op if unavailable)
cmd = "systemd-inhibit";
cmdArgs = [
"--what=idle:sleep",
"--who=SF",
"--why=Active SF session",
"sleep",
"infinity",
];
} else {
ctx.ui.notify(
"Keep-alive is supported on macOS (caffeinate) and Linux (systemd-inhibit).",
"info",
);
return;
}
try {
_keepAliveProcess = spawn(cmd, cmdArgs, {
stdio: "ignore",
detached: false,
});
_keepAliveProcess.on("error", () => {
_keepAliveProcess = null;
});
_keepAliveProcess.on("exit", () => {
_keepAliveProcess = null;
});
ctx.ui.notify(
`Keep-alive started (${cmd}). Run /keep-alive off to stop.`,
"info",
);
} catch {
ctx.ui.notify(
`Could not start ${cmd}. Make sure it is installed.`,
"warning",
);
}
}