@pandi-coding-agent/pandi-kitty 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +33 -0
- package/command.ts +6 -0
- package/index.ts +185 -0
- package/kitty.ts +288 -0
- package/notify.ts +13 -0
- package/package.json +41 -0
- package/skills/kitty-remote-control/SKILL.md +81 -0
package/README.md
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# pandi-kitty
|
|
2
|
+
|
|
3
|
+
Extensión de Pi para controlar el terminal [kitty](https://sw.kovidgoyal.net/kitty/)
|
|
4
|
+
en ejecución vía su protocolo de control remoto (`kitty @ ...`).
|
|
5
|
+
|
|
6
|
+
## Requisitos
|
|
7
|
+
|
|
8
|
+
`allow_remote_control yes` en `kitty.conf` (o `kitty -o allow_remote_control=yes`), y
|
|
9
|
+
correr Pi desde una sesión de kitty en ejecución.
|
|
10
|
+
|
|
11
|
+
## Comando
|
|
12
|
+
|
|
13
|
+
```
|
|
14
|
+
/kitty tab nueva tab
|
|
15
|
+
/kitty window nueva ventana (según el layout activo)
|
|
16
|
+
/kitty vsplit nueva ventana en split vertical
|
|
17
|
+
/kitty hsplit nueva ventana en split horizontal
|
|
18
|
+
/kitty os-window nueva ventana de OS
|
|
19
|
+
/kitty layout <nombre> cambia el layout activo (ej. splits, tall, fat, grid)
|
|
20
|
+
/kitty close [id] cierra una ventana (la activa si se omite el id)
|
|
21
|
+
/kitty focus <id> enfoca una ventana por id
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
## Tool
|
|
25
|
+
|
|
26
|
+
`kitty_remote` — acciones `launch`, `goto-layout`, `close-window`, `focus-window`.
|
|
27
|
+
Ver `index.ts` para el esquema de parámetros completo.
|
|
28
|
+
|
|
29
|
+
## Nota
|
|
30
|
+
|
|
31
|
+
`--location vsplit/hsplit` solo tiene efecto visible bajo el layout `splits`. Bajo
|
|
32
|
+
otros layouts (`tall`, `fat`, `grid`, ...) la ventana se abre igual, pero la
|
|
33
|
+
dirección del split la decide ese layout.
|
package/command.ts
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
/** Parseo puro de `/kitty <subcomando> [args...]` en acción + tokens. */
|
|
2
|
+
export function parseKittyCommand(input: string): { action: string; rest: string[] } {
|
|
3
|
+
const tokens = (input ?? "").trim().split(/\s+/).filter(Boolean);
|
|
4
|
+
const action = (tokens.shift() ?? "tab").toLowerCase();
|
|
5
|
+
return { action, rest: tokens };
|
|
6
|
+
}
|
package/index.ts
ADDED
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* pandi-kitty: controla el terminal kitty en ejecución desde Pi vía su protocolo
|
|
3
|
+
* de control remoto (`kitty @ ...`).
|
|
4
|
+
*
|
|
5
|
+
* Dos superficies (convención del proyecto; ver pandi-worktree):
|
|
6
|
+
* - `/kitty` comando slash para personas (tab | window | vsplit | hsplit | layout <nombre>)
|
|
7
|
+
* - `kitty_remote` tool invocable por el modelo (acciones explícitas)
|
|
8
|
+
*
|
|
9
|
+
* Ambas comparten los manejadores puros de `./kitty.ts`. `kitty` siempre se invoca con
|
|
10
|
+
* un array ARGV (nunca un string de shell).
|
|
11
|
+
*
|
|
12
|
+
* Requiere `allow_remote_control yes` en kitty.conf (o `-o allow_remote_control=yes` al
|
|
13
|
+
* arrancar kitty) y correr DESDE una sesión de kitty en ejecución.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import { StringEnum } from "@earendil-works/pi-ai";
|
|
17
|
+
import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
18
|
+
import { Type } from "typebox";
|
|
19
|
+
import { parseKittyCommand } from "./command.js";
|
|
20
|
+
import {
|
|
21
|
+
DEFAULT_KITTY_TIMEOUT_MS,
|
|
22
|
+
type HandlerResult,
|
|
23
|
+
parseTimeoutMs,
|
|
24
|
+
runCloseWindow,
|
|
25
|
+
runFocusWindow,
|
|
26
|
+
runGotoLayout,
|
|
27
|
+
runKitty,
|
|
28
|
+
runLaunch,
|
|
29
|
+
SPLIT_LOCATIONS,
|
|
30
|
+
WINDOW_TYPES,
|
|
31
|
+
} from "./kitty.js";
|
|
32
|
+
import { notify } from "./notify.js";
|
|
33
|
+
|
|
34
|
+
export { parseKittyCommand } from "./command.js";
|
|
35
|
+
export {
|
|
36
|
+
buildCloseWindowArgs,
|
|
37
|
+
buildFocusWindowArgs,
|
|
38
|
+
buildGotoLayoutArgs,
|
|
39
|
+
buildLaunchArgs,
|
|
40
|
+
describeError,
|
|
41
|
+
runCloseWindow,
|
|
42
|
+
runFocusWindow,
|
|
43
|
+
runGotoLayout,
|
|
44
|
+
runKitty,
|
|
45
|
+
runLaunch,
|
|
46
|
+
SPLIT_LOCATIONS,
|
|
47
|
+
WINDOW_TYPES,
|
|
48
|
+
} from "./kitty.js";
|
|
49
|
+
|
|
50
|
+
const HELP_TEXT = [
|
|
51
|
+
"Uso:",
|
|
52
|
+
" /kitty tab new tab",
|
|
53
|
+
" /kitty window nueva ventana (según el layout activo)",
|
|
54
|
+
" /kitty vsplit nueva ventana en split vertical",
|
|
55
|
+
" /kitty hsplit nueva ventana en split horizontal",
|
|
56
|
+
" /kitty os-window nueva ventana de OS",
|
|
57
|
+
" /kitty layout <nombre> cambia el layout activo (ej. splits, tall, fat, grid)",
|
|
58
|
+
" /kitty close [id] cierra una ventana (la activa si se omite el id)",
|
|
59
|
+
" /kitty focus <id> enfoca una ventana por id",
|
|
60
|
+
"",
|
|
61
|
+
"Requiere `allow_remote_control yes` en kitty.conf y correr desde una sesión de kitty.",
|
|
62
|
+
].join("\n");
|
|
63
|
+
|
|
64
|
+
function buildOpts(cwd: string, signal: AbortSignal | null | undefined) {
|
|
65
|
+
return {
|
|
66
|
+
cwd,
|
|
67
|
+
signal: signal ?? undefined,
|
|
68
|
+
timeoutMs: parseTimeoutMs(process.env.PI_KITTY_TIMEOUT_MS, DEFAULT_KITTY_TIMEOUT_MS),
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// --------------------------------------------------------------------------
|
|
73
|
+
// Handler del comando
|
|
74
|
+
// --------------------------------------------------------------------------
|
|
75
|
+
|
|
76
|
+
async function runCommand(ctx: ExtensionContext, input: string): Promise<void> {
|
|
77
|
+
const { action, rest } = parseKittyCommand(input);
|
|
78
|
+
const opts = buildOpts(ctx.cwd, ctx.signal);
|
|
79
|
+
|
|
80
|
+
if (action === "help" || action === "-h" || action === "--help") {
|
|
81
|
+
notify(ctx, HELP_TEXT, "info");
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
let result: HandlerResult;
|
|
86
|
+
switch (action) {
|
|
87
|
+
case "tab":
|
|
88
|
+
result = await runLaunch(runKitty, { type: "tab" }, opts);
|
|
89
|
+
break;
|
|
90
|
+
case "window":
|
|
91
|
+
result = await runLaunch(runKitty, { type: "window" }, opts);
|
|
92
|
+
break;
|
|
93
|
+
case "vsplit":
|
|
94
|
+
result = await runLaunch(runKitty, { type: "window", location: "vsplit" }, opts);
|
|
95
|
+
break;
|
|
96
|
+
case "hsplit":
|
|
97
|
+
result = await runLaunch(runKitty, { type: "window", location: "hsplit" }, opts);
|
|
98
|
+
break;
|
|
99
|
+
case "os-window":
|
|
100
|
+
result = await runLaunch(runKitty, { type: "os-window" }, opts);
|
|
101
|
+
break;
|
|
102
|
+
case "layout":
|
|
103
|
+
result = await runGotoLayout(runKitty, { layout: rest[0] ?? "" }, opts);
|
|
104
|
+
break;
|
|
105
|
+
case "close":
|
|
106
|
+
result = await runCloseWindow(runKitty, { matchId: rest[0] }, opts);
|
|
107
|
+
break;
|
|
108
|
+
case "focus":
|
|
109
|
+
result = await runFocusWindow(runKitty, { matchId: rest[0] ?? "" }, opts);
|
|
110
|
+
break;
|
|
111
|
+
default:
|
|
112
|
+
notify(ctx, `Subcomando desconocido: ${action}\n\n${HELP_TEXT}`, "warning");
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
115
|
+
notify(ctx, result.text, result.ok ? "info" : "error");
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// --------------------------------------------------------------------------
|
|
119
|
+
// Adaptador del resultado de la tool
|
|
120
|
+
// --------------------------------------------------------------------------
|
|
121
|
+
|
|
122
|
+
function toToolResult(result: HandlerResult) {
|
|
123
|
+
return { content: [{ type: "text" as const, text: result.text }], details: result.details };
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// --------------------------------------------------------------------------
|
|
127
|
+
// Entrada de la extensión
|
|
128
|
+
// --------------------------------------------------------------------------
|
|
129
|
+
|
|
130
|
+
export default function kittyExtension(pi: ExtensionAPI): void {
|
|
131
|
+
pi.registerCommand("kitty", {
|
|
132
|
+
description:
|
|
133
|
+
"Controlá kitty vía remote control: tab | window | vsplit | hsplit | os-window | layout | close | focus",
|
|
134
|
+
handler: async (args, ctx) => {
|
|
135
|
+
await runCommand(ctx, args);
|
|
136
|
+
},
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
pi.registerTool({
|
|
140
|
+
name: "kitty_remote",
|
|
141
|
+
label: "Control remoto de kitty",
|
|
142
|
+
description:
|
|
143
|
+
"Controlá el terminal kitty en ejecución vía su protocolo de control remoto (`kitty @ ...`). Acciones: 'launch' (abre tab/window/os-window nueva, opcionalmente con split vsplit/hsplit), 'goto-layout' (cambia el layout activo), 'close-window' (cierra una ventana), 'focus-window' (enfoca una ventana por id). Requiere `allow_remote_control yes` en kitty.conf y correr desde una sesión de kitty en ejecución.",
|
|
144
|
+
promptSnippet: "Controlá kitty (tabs, ventanas, splits, layout) vía su protocolo de control remoto.",
|
|
145
|
+
promptGuidelines: [
|
|
146
|
+
"Usá kitty_remote para abrir tabs/ventanas/splits de kitty o cambiar su layout en lugar de instruir al usuario a hacerlo manualmente.",
|
|
147
|
+
"Para 'launch' con type:'window', pasá location:'vsplit' o 'hsplit' para un split; el split solo se ve como tal si el layout activo es 'splits' (usá goto-layout primero si hace falta).",
|
|
148
|
+
"Si kitty_remote falla con un error de socket, es porque no hay remote control habilitado (allow_remote_control yes en kitty.conf) o no se está corriendo desde dentro de una sesión de kitty — no reintentes a ciegas, avisale al usuario.",
|
|
149
|
+
],
|
|
150
|
+
parameters: Type.Object({
|
|
151
|
+
action: StringEnum(["launch", "goto-layout", "close-window", "focus-window"] as const),
|
|
152
|
+
type: Type.Optional(StringEnum(WINDOW_TYPES, { description: "Para launch: tab | os-window | window." })),
|
|
153
|
+
location: Type.Optional(
|
|
154
|
+
StringEnum(SPLIT_LOCATIONS, { description: "Para launch con type:'window': vsplit | hsplit." }),
|
|
155
|
+
),
|
|
156
|
+
layout: Type.Optional(
|
|
157
|
+
Type.String({ description: "Para goto-layout: nombre del layout (ej. splits, tall, fat, grid)." }),
|
|
158
|
+
),
|
|
159
|
+
matchId: Type.Optional(
|
|
160
|
+
Type.String({ description: "Para close-window/focus-window: id de la ventana objetivo." }),
|
|
161
|
+
),
|
|
162
|
+
}),
|
|
163
|
+
executionMode: "sequential",
|
|
164
|
+
async execute(_toolCallId, params, signal, _onUpdate, ctx) {
|
|
165
|
+
const opts = buildOpts(ctx.cwd, signal);
|
|
166
|
+
switch (params.action) {
|
|
167
|
+
case "launch":
|
|
168
|
+
return toToolResult(
|
|
169
|
+
await runLaunch(runKitty, { type: params.type ?? "tab", location: params.location }, opts),
|
|
170
|
+
);
|
|
171
|
+
case "goto-layout":
|
|
172
|
+
return toToolResult(await runGotoLayout(runKitty, { layout: params.layout ?? "" }, opts));
|
|
173
|
+
case "close-window":
|
|
174
|
+
return toToolResult(await runCloseWindow(runKitty, { matchId: params.matchId }, opts));
|
|
175
|
+
case "focus-window":
|
|
176
|
+
return toToolResult(await runFocusWindow(runKitty, { matchId: params.matchId ?? "" }, opts));
|
|
177
|
+
default:
|
|
178
|
+
return {
|
|
179
|
+
content: [{ type: "text" as const, text: `Acción desconocida: ${params.action}` }],
|
|
180
|
+
details: { isError: true },
|
|
181
|
+
};
|
|
182
|
+
}
|
|
183
|
+
},
|
|
184
|
+
});
|
|
185
|
+
}
|
package/kitty.ts
ADDED
|
@@ -0,0 +1,288 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Utilidades puras + un único punto de ejecución para la extensión pandi-kitty.
|
|
3
|
+
*
|
|
4
|
+
* kitty expone su protocolo de control remoto vía `kitty @ <subcomando>`. Solo funciona
|
|
5
|
+
* desde una sesión de kitty en ejecución con `allow_remote_control` habilitado, y habla
|
|
6
|
+
* con esa instancia por un socket local.
|
|
7
|
+
* - `runKitty` lanza `kitty` con un array ARGV (nunca un string de shell).
|
|
8
|
+
* - `build*Args` son constructores puros de argv (testeados exactamente).
|
|
9
|
+
* - los manejadores `run*` reciben una fn `run` inyectada para que el despacho sea
|
|
10
|
+
* determinista en tests sin necesitar una sesión de kitty real.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { spawn } from "node:child_process";
|
|
14
|
+
|
|
15
|
+
// --------------------------------------------------------------------------
|
|
16
|
+
// Punto de spawn
|
|
17
|
+
// --------------------------------------------------------------------------
|
|
18
|
+
|
|
19
|
+
export interface KittyResult {
|
|
20
|
+
ok: boolean;
|
|
21
|
+
stdout: string;
|
|
22
|
+
stderr: string;
|
|
23
|
+
exitCode?: number;
|
|
24
|
+
timedOut?: boolean;
|
|
25
|
+
/** Se setea cuando nunca logramos hacer spawn de `kitty` (ej. no está instalado). */
|
|
26
|
+
spawnError?: string;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export interface RunKittyOptions {
|
|
30
|
+
cwd?: string;
|
|
31
|
+
signal?: AbortSignal;
|
|
32
|
+
timeoutMs?: number;
|
|
33
|
+
/** Binario a ejecutar; redefinible para que los tests apunten a un nombre garantizadamente ausente. */
|
|
34
|
+
bin?: string;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export const DEFAULT_KITTY_TIMEOUT_MS = 15_000;
|
|
38
|
+
export const MIN_KITTY_TIMEOUT_MS = 1_000;
|
|
39
|
+
|
|
40
|
+
export function parseTimeoutMs(raw: string | undefined, fallback = DEFAULT_KITTY_TIMEOUT_MS): number {
|
|
41
|
+
const n = Number(raw);
|
|
42
|
+
if (!Number.isFinite(n) || n <= 0) return fallback;
|
|
43
|
+
return Math.max(MIN_KITTY_TIMEOUT_MS, Math.floor(n));
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** Firma compartida por runKitty y el runner simulado inyectado en tests. */
|
|
47
|
+
export type RunKitty = (args: string[], options?: RunKittyOptions) => Promise<KittyResult>;
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Hace spawn de `kitty @ ...` con un array argv. Falla de spawn, exit no cero, timeout o
|
|
51
|
+
* abort vuelven como un KittyResult (nunca lanza).
|
|
52
|
+
*/
|
|
53
|
+
export function runKitty(args: string[], options: RunKittyOptions = {}): Promise<KittyResult> {
|
|
54
|
+
const { cwd, signal, timeoutMs = DEFAULT_KITTY_TIMEOUT_MS, bin = "kitty" } = options;
|
|
55
|
+
return new Promise((resolve) => {
|
|
56
|
+
let stdout = "";
|
|
57
|
+
let stderr = "";
|
|
58
|
+
let settled = false;
|
|
59
|
+
let timedOut = false;
|
|
60
|
+
|
|
61
|
+
const child = spawn(bin, ["@", ...args], { cwd, windowsHide: true });
|
|
62
|
+
|
|
63
|
+
const finish = (result: KittyResult) => {
|
|
64
|
+
if (settled) return;
|
|
65
|
+
settled = true;
|
|
66
|
+
clearTimeout(timer);
|
|
67
|
+
if (signal) signal.removeEventListener("abort", onAbort);
|
|
68
|
+
resolve(result);
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
const onAbort = () => {
|
|
72
|
+
timedOut = true;
|
|
73
|
+
child.kill("SIGTERM");
|
|
74
|
+
};
|
|
75
|
+
if (signal) {
|
|
76
|
+
if (signal.aborted) onAbort();
|
|
77
|
+
else signal.addEventListener("abort", onAbort, { once: true });
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const timer = setTimeout(() => {
|
|
81
|
+
timedOut = true;
|
|
82
|
+
child.kill("SIGTERM");
|
|
83
|
+
}, timeoutMs);
|
|
84
|
+
|
|
85
|
+
child.stdout?.on("data", (chunk) => {
|
|
86
|
+
stdout += chunk.toString();
|
|
87
|
+
});
|
|
88
|
+
child.stderr?.on("data", (chunk) => {
|
|
89
|
+
stderr += chunk.toString();
|
|
90
|
+
});
|
|
91
|
+
child.on("error", (err: Error) => {
|
|
92
|
+
finish({ ok: false, stdout, stderr, spawnError: err.message });
|
|
93
|
+
});
|
|
94
|
+
child.on("close", (code) => {
|
|
95
|
+
finish({ ok: !timedOut && code === 0, stdout, stderr, exitCode: code ?? undefined, timedOut });
|
|
96
|
+
});
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// --------------------------------------------------------------------------
|
|
101
|
+
// Constructores de argv (puros)
|
|
102
|
+
// --------------------------------------------------------------------------
|
|
103
|
+
|
|
104
|
+
export const WINDOW_TYPES = ["tab", "os-window", "window"] as const;
|
|
105
|
+
export type WindowType = (typeof WINDOW_TYPES)[number];
|
|
106
|
+
|
|
107
|
+
export const SPLIT_LOCATIONS = ["vsplit", "hsplit"] as const;
|
|
108
|
+
export type SplitLocation = (typeof SPLIT_LOCATIONS)[number];
|
|
109
|
+
|
|
110
|
+
export interface LaunchOptions {
|
|
111
|
+
type: WindowType;
|
|
112
|
+
location?: SplitLocation;
|
|
113
|
+
cwd?: string;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
export function buildLaunchArgs(opts: LaunchOptions): string[] {
|
|
117
|
+
const args = ["launch", "--type", opts.type];
|
|
118
|
+
if (opts.location) args.push("--location", opts.location);
|
|
119
|
+
if (opts.cwd) args.push("--cwd", opts.cwd);
|
|
120
|
+
return args;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
export function buildGotoLayoutArgs(layout: string): string[] {
|
|
124
|
+
return ["goto-layout", layout];
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
export function buildCloseWindowArgs(opts: { matchId?: string } = {}): string[] {
|
|
128
|
+
const args = ["close-window"];
|
|
129
|
+
if (opts.matchId) args.push("--match", `id:${opts.matchId}`);
|
|
130
|
+
return args;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
export function buildFocusWindowArgs(matchId: string): string[] {
|
|
134
|
+
return ["focus-window", "--match", `id:${matchId}`];
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// --------------------------------------------------------------------------
|
|
138
|
+
// Normalización de errores
|
|
139
|
+
// --------------------------------------------------------------------------
|
|
140
|
+
|
|
141
|
+
const NOT_RUNNING_HINT =
|
|
142
|
+
"No se pudo hablar con kitty por el socket de control remoto. Verificá que estés dentro de una sesión de kitty y que `allow_remote_control yes` esté en kitty.conf (o iniciá kitty con -o allow_remote_control=yes).";
|
|
143
|
+
|
|
144
|
+
/** Convierte un KittyResult fallido en una sola línea acotada y accionable. */
|
|
145
|
+
export function describeError(result: KittyResult, action: string): string {
|
|
146
|
+
if (result.spawnError) {
|
|
147
|
+
if (/ENOENT/i.test(result.spawnError)) return "No se encontró el binario `kitty` en el PATH.";
|
|
148
|
+
return `No se pudo ejecutar \`kitty @ ${action}\`: ${result.spawnError}`;
|
|
149
|
+
}
|
|
150
|
+
if (result.timedOut) return `\`kitty @ ${action}\` agotó el tiempo de espera.`;
|
|
151
|
+
const detail = (result.stderr || result.stdout || "").trim();
|
|
152
|
+
if (/no such remote control socket|could not connect|is kitty running/i.test(detail)) return NOT_RUNNING_HINT;
|
|
153
|
+
return detail
|
|
154
|
+
? `\`kitty @ ${action}\` falló: ${detail}`
|
|
155
|
+
: `\`kitty @ ${action}\` falló (salida ${result.exitCode ?? "?"}).`;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// --------------------------------------------------------------------------
|
|
159
|
+
// Manejadores de alto nivel (reciben un runner inyectado; por lo demás, despacho puro)
|
|
160
|
+
// --------------------------------------------------------------------------
|
|
161
|
+
|
|
162
|
+
export interface HandlerResult {
|
|
163
|
+
ok: boolean;
|
|
164
|
+
text: string;
|
|
165
|
+
details: Record<string, unknown>;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
export interface HandlerOpts {
|
|
169
|
+
cwd?: string;
|
|
170
|
+
signal?: AbortSignal;
|
|
171
|
+
timeoutMs?: number;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
function isWindowType(type: string): type is WindowType {
|
|
175
|
+
return (WINDOW_TYPES as readonly string[]).includes(type);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
function isSplitLocation(location: string): location is SplitLocation {
|
|
179
|
+
return (SPLIT_LOCATIONS as readonly string[]).includes(location);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
export async function runLaunch(
|
|
183
|
+
run: RunKitty,
|
|
184
|
+
params: { type: string; location?: string; cwd?: string },
|
|
185
|
+
opts: HandlerOpts,
|
|
186
|
+
): Promise<HandlerResult> {
|
|
187
|
+
if (!isWindowType(params.type)) {
|
|
188
|
+
return {
|
|
189
|
+
ok: false,
|
|
190
|
+
text: `Tipo de ventana desconocido "${params.type}". Tipos válidos: ${WINDOW_TYPES.join(", ")}.`,
|
|
191
|
+
details: { isError: true, action: "launch" },
|
|
192
|
+
};
|
|
193
|
+
}
|
|
194
|
+
if (params.location && !isSplitLocation(params.location)) {
|
|
195
|
+
return {
|
|
196
|
+
ok: false,
|
|
197
|
+
text: `Ubicación de split desconocida "${params.location}". Ubicaciones válidas: ${SPLIT_LOCATIONS.join(", ")}.`,
|
|
198
|
+
details: { isError: true, action: "launch" },
|
|
199
|
+
};
|
|
200
|
+
}
|
|
201
|
+
const result = await run(
|
|
202
|
+
buildLaunchArgs({ type: params.type, location: params.location as SplitLocation | undefined, cwd: params.cwd }),
|
|
203
|
+
opts,
|
|
204
|
+
);
|
|
205
|
+
if (!result.ok) {
|
|
206
|
+
return { ok: false, text: describeError(result, "launch"), details: { isError: true, action: "launch" } };
|
|
207
|
+
}
|
|
208
|
+
const id = result.stdout.trim();
|
|
209
|
+
return {
|
|
210
|
+
ok: true,
|
|
211
|
+
text: `Se abrió ${params.type} nueva (id ${id || "?"}).`,
|
|
212
|
+
details: { action: "launch", type: params.type, location: params.location, id },
|
|
213
|
+
};
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
export async function runGotoLayout(
|
|
217
|
+
run: RunKitty,
|
|
218
|
+
params: { layout: string },
|
|
219
|
+
opts: HandlerOpts,
|
|
220
|
+
): Promise<HandlerResult> {
|
|
221
|
+
if (!params.layout) {
|
|
222
|
+
return {
|
|
223
|
+
ok: false,
|
|
224
|
+
text: "goto-layout requiere un nombre de layout.",
|
|
225
|
+
details: { isError: true, action: "goto-layout" },
|
|
226
|
+
};
|
|
227
|
+
}
|
|
228
|
+
const result = await run(buildGotoLayoutArgs(params.layout), opts);
|
|
229
|
+
if (!result.ok) {
|
|
230
|
+
return {
|
|
231
|
+
ok: false,
|
|
232
|
+
text: describeError(result, "goto-layout"),
|
|
233
|
+
details: { isError: true, action: "goto-layout" },
|
|
234
|
+
};
|
|
235
|
+
}
|
|
236
|
+
return {
|
|
237
|
+
ok: true,
|
|
238
|
+
text: `Layout activo: ${params.layout}.`,
|
|
239
|
+
details: { action: "goto-layout", layout: params.layout },
|
|
240
|
+
};
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
export async function runCloseWindow(
|
|
244
|
+
run: RunKitty,
|
|
245
|
+
params: { matchId?: string },
|
|
246
|
+
opts: HandlerOpts,
|
|
247
|
+
): Promise<HandlerResult> {
|
|
248
|
+
const result = await run(buildCloseWindowArgs(params), opts);
|
|
249
|
+
if (!result.ok) {
|
|
250
|
+
return {
|
|
251
|
+
ok: false,
|
|
252
|
+
text: describeError(result, "close-window"),
|
|
253
|
+
details: { isError: true, action: "close-window" },
|
|
254
|
+
};
|
|
255
|
+
}
|
|
256
|
+
return {
|
|
257
|
+
ok: true,
|
|
258
|
+
text: params.matchId ? `Se cerró la ventana ${params.matchId}.` : "Se cerró la ventana activa.",
|
|
259
|
+
details: { action: "close-window", matchId: params.matchId },
|
|
260
|
+
};
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
export async function runFocusWindow(
|
|
264
|
+
run: RunKitty,
|
|
265
|
+
params: { matchId: string },
|
|
266
|
+
opts: HandlerOpts,
|
|
267
|
+
): Promise<HandlerResult> {
|
|
268
|
+
if (!params.matchId) {
|
|
269
|
+
return {
|
|
270
|
+
ok: false,
|
|
271
|
+
text: "focus-window requiere un id de ventana.",
|
|
272
|
+
details: { isError: true, action: "focus-window" },
|
|
273
|
+
};
|
|
274
|
+
}
|
|
275
|
+
const result = await run(buildFocusWindowArgs(params.matchId), opts);
|
|
276
|
+
if (!result.ok) {
|
|
277
|
+
return {
|
|
278
|
+
ok: false,
|
|
279
|
+
text: describeError(result, "focus-window"),
|
|
280
|
+
details: { isError: true, action: "focus-window" },
|
|
281
|
+
};
|
|
282
|
+
}
|
|
283
|
+
return {
|
|
284
|
+
ok: true,
|
|
285
|
+
text: `Foco en la ventana ${params.matchId}.`,
|
|
286
|
+
details: { action: "focus-window", matchId: params.matchId },
|
|
287
|
+
};
|
|
288
|
+
}
|
package/notify.ts
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
|
|
3
|
+
export function notify(ctx: ExtensionContext, message: string, type: "info" | "warning" | "error" = "info"): void {
|
|
4
|
+
if (ctx.mode === "print") {
|
|
5
|
+
(type === "info" ? console.log : console.error)(message);
|
|
6
|
+
return;
|
|
7
|
+
}
|
|
8
|
+
if (ctx.hasUI) {
|
|
9
|
+
ctx.ui.notify(message, type);
|
|
10
|
+
return;
|
|
11
|
+
}
|
|
12
|
+
if (type !== "info") console.error(message);
|
|
13
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@pandi-coding-agent/pandi-kitty",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Extensión de Pi para controlar el terminal kitty vía su protocolo de control remoto: un comando /kitty y una herramienta kitty_remote invocable por el modelo (tabs, ventanas, splits, layout).",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"repository": {
|
|
7
|
+
"type": "git",
|
|
8
|
+
"url": "git+https://github.com/andrestobelem/pandi-extensions.git",
|
|
9
|
+
"directory": "extensions/pandi-kitty"
|
|
10
|
+
},
|
|
11
|
+
"type": "module",
|
|
12
|
+
"keywords": [
|
|
13
|
+
"pi-package",
|
|
14
|
+
"pi",
|
|
15
|
+
"pi-extension",
|
|
16
|
+
"kitty",
|
|
17
|
+
"terminal",
|
|
18
|
+
"remote-control"
|
|
19
|
+
],
|
|
20
|
+
"files": [
|
|
21
|
+
"index.ts",
|
|
22
|
+
"command.ts",
|
|
23
|
+
"kitty.ts",
|
|
24
|
+
"notify.ts",
|
|
25
|
+
"skills",
|
|
26
|
+
"README.md"
|
|
27
|
+
],
|
|
28
|
+
"pi": {
|
|
29
|
+
"extensions": [
|
|
30
|
+
"./index.ts"
|
|
31
|
+
],
|
|
32
|
+
"skills": [
|
|
33
|
+
"./skills"
|
|
34
|
+
]
|
|
35
|
+
},
|
|
36
|
+
"peerDependencies": {
|
|
37
|
+
"@earendil-works/pi-ai": "^0.80.3",
|
|
38
|
+
"@earendil-works/pi-coding-agent": "^0.80.3",
|
|
39
|
+
"typebox": "^1.1.38"
|
|
40
|
+
}
|
|
41
|
+
}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: kitty-remote-control
|
|
3
|
+
description: >-
|
|
4
|
+
Controla la terminal kitty desde el shell mediante su protocolo de
|
|
5
|
+
remote-control (`kitty @ ...`): abre tabs, ventanas y splits nuevos, y
|
|
6
|
+
consulta/administra la instancia de kitty en ejecución. Usar cuando el
|
|
7
|
+
usuario pida abrir una tab o ventana nueva de kitty, dividir paneles
|
|
8
|
+
(vertical/horizontal), o scriptear kitty desde una sesión en curso. Si la
|
|
9
|
+
extensión pandi-kitty está instalada, preferí su comando `/kitty` o la tool
|
|
10
|
+
`kitty_remote` antes que shell ad hoc.
|
|
11
|
+
---
|
|
12
|
+
|
|
13
|
+
# Control remoto de kitty
|
|
14
|
+
|
|
15
|
+
kitty trae un protocolo de remote-control manejado por el subcomando
|
|
16
|
+
`kitty @`. Habla con la instancia de kitty *actualmente en ejecución* a
|
|
17
|
+
través de un socket de control, así que solo funciona desde dentro de una
|
|
18
|
+
sesión kitty que tenga el remote control habilitado. 🐼
|
|
19
|
+
|
|
20
|
+
## Prerequisito
|
|
21
|
+
|
|
22
|
+
El remote control tiene que estar permitido. Revisá/habilitalo en `kitty.conf`:
|
|
23
|
+
|
|
24
|
+
```
|
|
25
|
+
allow_remote_control yes
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
O iniciá kitty con `-o allow_remote_control=yes`. Sin esto, todo comando
|
|
29
|
+
`kitty @ ...` falla con un error de socket/permisos.
|
|
30
|
+
|
|
31
|
+
## Comandos principales
|
|
32
|
+
|
|
33
|
+
```bash
|
|
34
|
+
# Tab nueva en la ventana actual
|
|
35
|
+
kitty @ launch --type=tab
|
|
36
|
+
|
|
37
|
+
# Ventana de OS nueva
|
|
38
|
+
kitty @ launch --type=os-window
|
|
39
|
+
|
|
40
|
+
# Ventana nueva en la tab actual (usa el layout activo)
|
|
41
|
+
kitty @ launch --type=window
|
|
42
|
+
|
|
43
|
+
# Split vertical (solo tiene efecto bajo el layout `splits`)
|
|
44
|
+
kitty @ launch --type=window --location=vsplit
|
|
45
|
+
|
|
46
|
+
# Split horizontal
|
|
47
|
+
kitty @ launch --type=window --location=hsplit
|
|
48
|
+
|
|
49
|
+
# Forzar el layout splits primero si el layout activo no es splits
|
|
50
|
+
kitty @ goto-layout splits
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
Cada llamada a `launch` imprime el id de la tab/ventana nueva si tiene éxito
|
|
54
|
+
(p. ej. `2`), que se puede usar con otros subcomandos de `kitty @`
|
|
55
|
+
(`focus-window`, `close-window`, `send-text`, etc.) para apuntarle con
|
|
56
|
+
precisión.
|
|
57
|
+
|
|
58
|
+
## Trampas
|
|
59
|
+
|
|
60
|
+
- `--location=vsplit`/`hsplit` solo tiene efecto visible bajo el layout
|
|
61
|
+
`splits`. Bajo `tall`, `fat`, `grid`, etc. la ventana nueva igual se abre,
|
|
62
|
+
pero la dirección del split la decide ese layout en su lugar.
|
|
63
|
+
- Todos los comandos `kitty @` son no-ops desde fuera de kitty (por ejemplo,
|
|
64
|
+
desde un script desacoplado o una terminal distinta) — necesitan que el
|
|
65
|
+
socket de un proceso kitty vivo sea alcanzable, lo cual normalmente
|
|
66
|
+
significa correr desde un shell dentro de esa instancia kitty.
|
|
67
|
+
- `--type=window` sin `--location` simplemente usa lo que el layout actual
|
|
68
|
+
haga con una ventana nueva (puede no verse como un "split" en absoluto).
|
|
69
|
+
|
|
70
|
+
## Integración en Pi
|
|
71
|
+
|
|
72
|
+
En este repo, `extensions/pandi-kitty/` envuelve estos comandos como paquete
|
|
73
|
+
standalone:
|
|
74
|
+
|
|
75
|
+
- comando `/kitty` para personas (`tab`, `window`, `vsplit`, `hsplit`,
|
|
76
|
+
`os-window`, `layout`, `close`, `focus`)
|
|
77
|
+
- tool `kitty_remote` para el modelo, con acciones `launch`, `goto-layout`,
|
|
78
|
+
`close-window` y `focus-window`
|
|
79
|
+
|
|
80
|
+
Usá la extensión cuando esté disponible; reservá `kitty @ ...` directo para
|
|
81
|
+
casos de diagnóstico o cuando la extensión no esté instalada.
|