@xynogen/pix-commands 0.3.6 → 0.3.8
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 +25 -0
- package/package.json +2 -2
- package/src/afk.ts +35 -0
- package/src/btw/render.ts +8 -7
- package/src/btw/widget.ts +2 -1
- package/src/extension.test.ts +38 -6
- package/src/extension.ts +2 -0
package/README.md
CHANGED
|
@@ -4,6 +4,7 @@ Pi extension providing focused slash commands:
|
|
|
4
4
|
|
|
5
5
|
- `/clear` — flush Pi's cached model data.
|
|
6
6
|
- `/btw <question>` — ask an isolated side question without interrupting the main agent.
|
|
7
|
+
- `/afk` — toggle AFK mode for unattended runs.
|
|
7
8
|
|
|
8
9
|
## `/clear`
|
|
9
10
|
|
|
@@ -28,6 +29,30 @@ The child session:
|
|
|
28
29
|
|
|
29
30
|
When the main agent is streaming, completion is shown as a notification and the durable card is appended after the main session becomes idle. This prevents the side answer from becoming steering input.
|
|
30
31
|
|
|
32
|
+
## `/afk`
|
|
33
|
+
|
|
34
|
+
`/afk` toggles AFK mode (away-from-keyboard) for unattended runs. It is a shared
|
|
35
|
+
toggle stored in a global flag and shown in the status bar.
|
|
36
|
+
|
|
37
|
+
When ON, yellow (medium-risk) permission gates auto-allow, while red (dangerous)
|
|
38
|
+
gates and sudo prompts auto-deny:
|
|
39
|
+
|
|
40
|
+
```text
|
|
41
|
+
AFK mode on — yellow gates auto-allow; red and sudo auto-deny.
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
When OFF (the default), normal approval prompts are restored:
|
|
45
|
+
|
|
46
|
+
```text
|
|
47
|
+
AFK mode off — approval prompts restored.
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
The point is to let the agent keep working on safe, medium-risk actions while you
|
|
51
|
+
are away, without silently permitting destructive ones. It pairs with the herdr
|
|
52
|
+
notification bridge in `pix-runtime`: you still get pinged when something truly
|
|
53
|
+
needs you (a red gate or sudo), and auto-deny turns that into a stop rather than
|
|
54
|
+
an approval.
|
|
55
|
+
|
|
31
56
|
## Install
|
|
32
57
|
|
|
33
58
|
```bash
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@xynogen/pix-commands",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.8",
|
|
4
4
|
"description": "Pi extension — slash commands for cache clearing and isolated side questions",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "src/index.ts",
|
|
@@ -46,6 +46,6 @@
|
|
|
46
46
|
},
|
|
47
47
|
"dependencies": {
|
|
48
48
|
"@xynogen/pix-pretty": "^1.13.0",
|
|
49
|
-
"@xynogen/pix-runtime": "^0.
|
|
49
|
+
"@xynogen/pix-runtime": "^0.6.0"
|
|
50
50
|
}
|
|
51
51
|
}
|
package/src/afk.ts
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import { icon } from "@xynogen/pix-pretty/icon-catalog";
|
|
3
|
+
|
|
4
|
+
const STATUS_KEY = "afk";
|
|
5
|
+
|
|
6
|
+
type AfkGlobal = typeof globalThis & { __pixAfk?: boolean };
|
|
7
|
+
type StatusUI = {
|
|
8
|
+
theme: { fg(color: "error", text: string): string };
|
|
9
|
+
setStatus(key: string, text: string | undefined): void;
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
function setAfkStatus(ui: StatusUI, active: boolean): void {
|
|
13
|
+
ui.setStatus(STATUS_KEY, active ? ui.theme.fg("error", `${icon("afk")} AFK`) : undefined);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export default function registerAfk(pi: ExtensionAPI): void {
|
|
17
|
+
pi.on("session_start", (_event, ctx) => {
|
|
18
|
+
setAfkStatus(ctx.ui, (globalThis as AfkGlobal).__pixAfk === true);
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
pi.registerCommand("afk", {
|
|
22
|
+
description: "Toggle unattended gate behavior",
|
|
23
|
+
handler: async (_args, ctx) => {
|
|
24
|
+
const state = !(globalThis as AfkGlobal).__pixAfk;
|
|
25
|
+
(globalThis as AfkGlobal).__pixAfk = state;
|
|
26
|
+
setAfkStatus(ctx.ui, state);
|
|
27
|
+
ctx.ui.notify(
|
|
28
|
+
state
|
|
29
|
+
? "AFK mode on — yellow gates auto-allow; red and sudo auto-deny."
|
|
30
|
+
: "AFK mode off — approval prompts restored.",
|
|
31
|
+
state ? "warning" : "info",
|
|
32
|
+
);
|
|
33
|
+
},
|
|
34
|
+
});
|
|
35
|
+
}
|
package/src/btw/render.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { getMarkdownTheme } from "@earendil-works/pi-coding-agent";
|
|
2
2
|
import { Box, Markdown, Spacer, Text } from "@earendil-works/pi-tui";
|
|
3
3
|
import { icon } from "@xynogen/pix-pretty/icon-catalog";
|
|
4
|
+
import { dotJoin } from "@xynogen/pix-pretty/utils";
|
|
4
5
|
import { formatDuration as fmtDuration } from "@xynogen/pix-pretty/widget-format";
|
|
5
6
|
|
|
6
7
|
export interface BtwMessageDetails {
|
|
@@ -33,8 +34,12 @@ export function registerBtwRenderer(
|
|
|
33
34
|
const statusGlyph = failed
|
|
34
35
|
? theme.fg("error", icon("status.error"))
|
|
35
36
|
: theme.fg("success", icon("status.ok"));
|
|
36
|
-
const meta = [
|
|
37
|
-
|
|
37
|
+
const meta = dotJoin([
|
|
38
|
+
details.model,
|
|
39
|
+
details.thinkingLevel,
|
|
40
|
+
formatDuration(details.durationMs),
|
|
41
|
+
details.toolUses > 0 && `${details.toolUses} tools`,
|
|
42
|
+
]);
|
|
38
43
|
|
|
39
44
|
// A custom renderer bypasses Pi's default custom-message box, so provide
|
|
40
45
|
// our own card. Use selectedBg rather than the generic custom-message
|
|
@@ -44,11 +49,7 @@ export function registerBtwRenderer(
|
|
|
44
49
|
// header text embedded inside Markdown can confuse wrapping and parsing.
|
|
45
50
|
const card = new Box(1, 1, (text) => theme.bg("selectedBg", text));
|
|
46
51
|
card.addChild(
|
|
47
|
-
new Text(
|
|
48
|
-
`${statusGlyph} ${theme.bold("BTW")} ${theme.fg("dim", `· ${meta.join(" · ")}`)}`,
|
|
49
|
-
0,
|
|
50
|
-
0,
|
|
51
|
-
),
|
|
52
|
+
new Text(`${statusGlyph} ${theme.bold("BTW")} ${theme.fg("dim", `· ${meta}`)}`, 0, 0),
|
|
52
53
|
);
|
|
53
54
|
card.addChild(
|
|
54
55
|
new Text(`${theme.fg("accent", "▐")} ${theme.fg("muted", details.question)}`, 0, 0),
|
package/src/btw/widget.ts
CHANGED
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
|
|
10
10
|
import { truncateToWidth } from "@earendil-works/pi-tui";
|
|
11
11
|
import { icon } from "@xynogen/pix-pretty/icon-catalog";
|
|
12
|
+
import { dotJoin } from "@xynogen/pix-pretty/utils";
|
|
12
13
|
import {
|
|
13
14
|
type ContextUsageLike,
|
|
14
15
|
describeActivity,
|
|
@@ -73,7 +74,7 @@ function statsFor(job: BtwWidgetJob, endMs: number): string {
|
|
|
73
74
|
const speed = formatSpeed(job.outputTokens, endMs - job.startedAt);
|
|
74
75
|
if (speed) parts.push(speed);
|
|
75
76
|
parts.push(formatMs(endMs - job.startedAt));
|
|
76
|
-
return parts
|
|
77
|
+
return dotJoin(parts);
|
|
77
78
|
}
|
|
78
79
|
|
|
79
80
|
function finishedLine(job: BtwWidgetJob, theme: WidgetTheme): string {
|
package/src/extension.test.ts
CHANGED
|
@@ -1,32 +1,39 @@
|
|
|
1
1
|
import { afterEach, describe, expect, test } from "bun:test";
|
|
2
2
|
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
3
|
+
import { icon } from "@xynogen/pix-pretty/icon-catalog";
|
|
3
4
|
import extension from "./extension.ts";
|
|
4
5
|
|
|
5
6
|
afterEach(() => {
|
|
6
7
|
delete (globalThis as { __pixOnce?: WeakMap<object, Set<string>> }).__pixOnce;
|
|
8
|
+
delete (globalThis as { __pixAfk?: boolean }).__pixAfk;
|
|
7
9
|
});
|
|
8
10
|
|
|
9
11
|
describe("pix-commands registration", () => {
|
|
10
12
|
function host() {
|
|
11
13
|
const commands: string[] = [];
|
|
14
|
+
const handlers = new Map<string, (args: string, ctx: never) => Promise<void>>();
|
|
12
15
|
const renderers: string[] = [];
|
|
13
16
|
const pi = {
|
|
14
|
-
registerCommand(
|
|
17
|
+
registerCommand(
|
|
18
|
+
name: string,
|
|
19
|
+
options: { handler?: (args: string, ctx: never) => Promise<void> },
|
|
20
|
+
) {
|
|
15
21
|
commands.push(name);
|
|
22
|
+
if (options.handler) handlers.set(name, options.handler);
|
|
16
23
|
},
|
|
17
24
|
registerEntryRenderer(name: string) {
|
|
18
25
|
renderers.push(name);
|
|
19
26
|
},
|
|
20
27
|
on() {},
|
|
21
28
|
} as unknown as ExtensionAPI;
|
|
22
|
-
return { pi, commands, renderers };
|
|
29
|
+
return { pi, commands, handlers, renderers };
|
|
23
30
|
}
|
|
24
31
|
|
|
25
|
-
test("registers /clear, /btw, and the BTW renderer once per Pi instance", () => {
|
|
32
|
+
test("registers /clear, /btw, /afk, and the BTW renderer once per Pi instance", () => {
|
|
26
33
|
const { pi, commands, renderers } = host();
|
|
27
34
|
extension(pi);
|
|
28
35
|
extension(pi);
|
|
29
|
-
expect(commands).toEqual(["clear", "btw"]);
|
|
36
|
+
expect(commands).toEqual(["clear", "btw", "afk"]);
|
|
30
37
|
expect(renderers).toEqual(["pix-btw-answer"]);
|
|
31
38
|
});
|
|
32
39
|
|
|
@@ -35,7 +42,32 @@ describe("pix-commands registration", () => {
|
|
|
35
42
|
const second = host();
|
|
36
43
|
extension(first.pi);
|
|
37
44
|
extension(second.pi);
|
|
38
|
-
expect(first.commands).toEqual(["clear", "btw"]);
|
|
39
|
-
expect(second.commands).toEqual(["clear", "btw"]);
|
|
45
|
+
expect(first.commands).toEqual(["clear", "btw", "afk"]);
|
|
46
|
+
expect(second.commands).toEqual(["clear", "btw", "afk"]);
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
test("/afk toggles shared state and status", async () => {
|
|
50
|
+
const { pi, handlers } = host();
|
|
51
|
+
extension(pi);
|
|
52
|
+
const statuses: Array<string | undefined> = [];
|
|
53
|
+
const notices: string[] = [];
|
|
54
|
+
const ctx = {
|
|
55
|
+
ui: {
|
|
56
|
+
theme: { fg: (color: string, text: string) => `<${color}>${text}</${color}>` },
|
|
57
|
+
setStatus: (_key: string, text: string | undefined) => statuses.push(text),
|
|
58
|
+
notify: (text: string) => notices.push(text),
|
|
59
|
+
},
|
|
60
|
+
};
|
|
61
|
+
const handler = handlers.get("afk");
|
|
62
|
+
if (!handler) throw new Error("/afk not registered");
|
|
63
|
+
|
|
64
|
+
await handler("", ctx as never);
|
|
65
|
+
expect((globalThis as { __pixAfk?: boolean }).__pixAfk).toBe(true);
|
|
66
|
+
expect(statuses.at(-1)).toBe(`<error>${icon("afk")} AFK</error>`);
|
|
67
|
+
expect(notices.at(-1)).toContain("yellow gates auto-allow");
|
|
68
|
+
|
|
69
|
+
await handler("", ctx as never);
|
|
70
|
+
expect((globalThis as { __pixAfk?: boolean }).__pixAfk).toBe(false);
|
|
71
|
+
expect(statuses.at(-1)).toBeUndefined();
|
|
40
72
|
});
|
|
41
73
|
});
|
package/src/extension.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
2
2
|
import { once } from "@xynogen/pix-runtime/once";
|
|
3
|
+
import registerAfk from "./afk.ts";
|
|
3
4
|
import { registerBtw } from "./btw/index.ts";
|
|
4
5
|
import registerClear from "./clear.ts";
|
|
5
6
|
|
|
@@ -7,5 +8,6 @@ export default function (pi: ExtensionAPI): void {
|
|
|
7
8
|
once(pi, "pix-commands", () => {
|
|
8
9
|
registerClear(pi);
|
|
9
10
|
registerBtw(pi);
|
|
11
|
+
registerAfk(pi);
|
|
10
12
|
});
|
|
11
13
|
}
|