@ryan_nookpi/pi-extension-auto-name 0.1.0 → 0.1.2

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 CHANGED
@@ -1,15 +1,28 @@
1
1
  # @ryan_nookpi/pi-extension-auto-name
2
2
 
3
- Auto session name extension for pi.
3
+ This extension automatically names a pi session based on the first user message.
4
+
5
+ It helps you quickly recognize what each session is about when many tasks are open at once.
4
6
 
5
7
  ## Install
6
8
 
7
9
  ```bash
8
- pi install /Users/creatrip/Documents/pi-extension/packages/auto-name
9
10
  pi install npm:@ryan_nookpi/pi-extension-auto-name
10
11
  ```
11
12
 
12
- ## What it provides
13
+ ## Great for
14
+
15
+ - quickly understanding what a session is about
16
+ - avoiding manual naming with `/name`
17
+ - showing the current task clearly in the terminal title and status area
18
+
19
+ ## How it works
20
+
21
+ - It reads the first user message and generates a short session name.
22
+ - The generated name is applied to the session name, status area, and terminal title.
23
+ - If a session already has a name, it does not overwrite it.
24
+ - It skips automatic naming for subagent sessions.
25
+
26
+ ## Example
13
27
 
14
- - Automatic session name detection
15
- - `./index.ts` entry
28
+ If the first request is something like `Prepare a pre-release checklist`, pi can automatically turn that into a short session title.
package/index.ts CHANGED
@@ -43,11 +43,8 @@ export default function autoSessionName(pi: ExtensionAPI) {
43
43
  if (!ctx.hasUI) return;
44
44
  const cwdBasename = path.basename(process.cwd());
45
45
  const name = pi.getSessionName();
46
- if (name) {
47
- ctx.ui.setTitle(`π - ${name} - ${cwdBasename}`);
48
- } else {
49
- ctx.ui.setTitle(`π - ${cwdBasename}`);
50
- }
46
+ if (!name) return;
47
+ ctx.ui.setTitle(`π - ${name} - ${cwdBasename}`);
51
48
  };
52
49
 
53
50
  const updateStatus = (ctx: ExtensionContext) => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ryan_nookpi/pi-extension-auto-name",
3
- "version": "0.1.0",
3
+ "version": "0.1.2",
4
4
  "description": "Auto session name extension for pi.",
5
5
  "type": "module",
6
6
  "keywords": [
@@ -0,0 +1,66 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import {
3
+ buildNameContext,
4
+ extractNameFromResult,
5
+ extractSessionFilePath,
6
+ formatNameStatus,
7
+ isSubagentSessionPath,
8
+ isSuccessfulResult,
9
+ MAX_MESSAGE_LENGTH,
10
+ MAX_NAME_LENGTH,
11
+ MAX_STATUS_CHARS,
12
+ SUBAGENT_SESSION_DIR,
13
+ } from "./auto-name-utils.ts";
14
+
15
+ describe("auto-name utils", () => {
16
+ it("detects subagent session paths", () => {
17
+ expect(isSubagentSessionPath(`${SUBAGENT_SESSION_DIR}/child/session.json`)).toBe(true);
18
+ expect(isSubagentSessionPath("/tmp/session.json")).toBe(false);
19
+ expect(isSubagentSessionPath(undefined)).toBe(false);
20
+ });
21
+
22
+ it("extracts and sanitizes the session file path", () => {
23
+ const sessionManager = {
24
+ getSessionFile: () => "\n /tmp/example.json \t",
25
+ };
26
+ expect(extractSessionFilePath(sessionManager)).toBe("/tmp/example.json");
27
+ expect(extractSessionFilePath({ getSessionFile: () => undefined })).toBeUndefined();
28
+ expect(extractSessionFilePath({ getSessionFile: "nope" })).toBeUndefined();
29
+ expect(extractSessionFilePath(null)).toBeUndefined();
30
+ expect(
31
+ extractSessionFilePath({
32
+ getSessionFile: () => {
33
+ throw new Error("boom");
34
+ },
35
+ }),
36
+ ).toBeUndefined();
37
+ });
38
+
39
+ it("formats the status line into a single clipped line", () => {
40
+ const noisy = ` alpha\n beta\t${"x".repeat(MAX_STATUS_CHARS)} `;
41
+ const formatted = formatNameStatus(noisy);
42
+ expect(formatted).not.toContain("\n");
43
+ expect(formatted.length).toBeLessThanOrEqual(MAX_STATUS_CHARS);
44
+ });
45
+
46
+ it("builds the name context with truncation", () => {
47
+ const message = "m".repeat(MAX_MESSAGE_LENGTH + 25);
48
+ const context = buildNameContext(message);
49
+ expect(context).toBe(`사용자 메시지: ${message.slice(0, MAX_MESSAGE_LENGTH)}`);
50
+ });
51
+
52
+ it("extracts text-only content and clips the result length", () => {
53
+ const result = extractNameFromResult([
54
+ { type: "text", text: ` ${"a".repeat(MAX_NAME_LENGTH)} ` },
55
+ { type: "image", text: "ignored" },
56
+ { type: "text", text: "suffix" },
57
+ ]);
58
+ expect(result).toBe(`${"a".repeat(MAX_NAME_LENGTH)}`);
59
+ });
60
+
61
+ it("accepts only fully stopped model results", () => {
62
+ expect(isSuccessfulResult("stop")).toBe(true);
63
+ expect(isSuccessfulResult("length")).toBe(false);
64
+ expect(isSuccessfulResult(undefined)).toBe(false);
65
+ });
66
+ });
@@ -0,0 +1,155 @@
1
+ import { completeSimple } from "@mariozechner/pi-ai";
2
+ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
3
+ import { generateShortLabel, type ShortLabelContext } from "./short-label.ts";
4
+
5
+ vi.mock("@mariozechner/pi-ai", () => ({
6
+ completeSimple: vi.fn(),
7
+ }));
8
+
9
+ const model = { id: "test-model" } as NonNullable<ShortLabelContext["model"]>;
10
+ type CompleteSimpleResult = Awaited<ReturnType<typeof completeSimple>>;
11
+
12
+ describe("generateShortLabel", () => {
13
+ beforeEach(() => {
14
+ vi.mocked(completeSimple).mockReset();
15
+ });
16
+
17
+ afterEach(() => {
18
+ vi.useRealTimers();
19
+ });
20
+
21
+ it("returns an empty string when model information or auth is unavailable", async () => {
22
+ expect(
23
+ await generateShortLabel(
24
+ {},
25
+ {
26
+ systemPrompt: "system",
27
+ prompt: "prompt",
28
+ },
29
+ ),
30
+ ).toBe("");
31
+
32
+ const label = await generateShortLabel(
33
+ {
34
+ model,
35
+ modelRegistry: {
36
+ getApiKeyAndHeaders: async () => ({ ok: false }),
37
+ },
38
+ },
39
+ {
40
+ systemPrompt: "system",
41
+ prompt: "prompt",
42
+ },
43
+ );
44
+
45
+ expect(label).toBe("");
46
+ expect(completeSimple).not.toHaveBeenCalled();
47
+ });
48
+
49
+ it("returns extracted text for successful completions", async () => {
50
+ vi.mocked(completeSimple).mockResolvedValue({
51
+ stopReason: "stop",
52
+ content: [
53
+ { type: "text", text: "first" },
54
+ { type: "text", text: " second" },
55
+ ],
56
+ } as CompleteSimpleResult);
57
+
58
+ const label = await generateShortLabel(
59
+ {
60
+ model,
61
+ modelRegistry: {
62
+ getApiKeyAndHeaders: async () => ({ ok: true, apiKey: "secret" }),
63
+ },
64
+ },
65
+ {
66
+ systemPrompt: "system",
67
+ prompt: "prompt",
68
+ },
69
+ );
70
+
71
+ expect(label).toBe("first second");
72
+ expect(completeSimple).toHaveBeenCalledTimes(1);
73
+ });
74
+
75
+ it("ignores incomplete model responses", async () => {
76
+ vi.mocked(completeSimple).mockResolvedValue({
77
+ stopReason: "length",
78
+ content: [{ type: "text", text: "ignored" }],
79
+ } as CompleteSimpleResult);
80
+
81
+ const label = await generateShortLabel(
82
+ {
83
+ model,
84
+ modelRegistry: {
85
+ getApiKeyAndHeaders: async () => ({ ok: true, apiKey: "secret" }),
86
+ },
87
+ },
88
+ {
89
+ systemPrompt: "system",
90
+ prompt: "prompt",
91
+ },
92
+ );
93
+
94
+ expect(label).toBe("");
95
+ });
96
+
97
+ it("falls back to the default text extractor and handles provider errors", async () => {
98
+ vi.mocked(completeSimple)
99
+ .mockResolvedValueOnce({
100
+ stopReason: "stop",
101
+ content: [
102
+ { type: "image", text: "ignored" },
103
+ { type: "text", text: " label " },
104
+ ],
105
+ } as CompleteSimpleResult)
106
+ .mockRejectedValueOnce(new Error("network"));
107
+
108
+ const ctx = {
109
+ model,
110
+ modelRegistry: {
111
+ getApiKeyAndHeaders: async () => ({ ok: true, apiKey: "secret", headers: { foo: "bar" } }),
112
+ },
113
+ };
114
+
115
+ const label = await generateShortLabel(ctx, {
116
+ systemPrompt: "system",
117
+ prompt: "prompt",
118
+ maxTokens: 10,
119
+ });
120
+ const failed = await generateShortLabel(ctx, {
121
+ systemPrompt: "system",
122
+ prompt: "prompt",
123
+ });
124
+
125
+ expect(label).toBe("label");
126
+ expect(failed).toBe("");
127
+ });
128
+
129
+ it("aborts when the timeout elapses", async () => {
130
+ vi.useFakeTimers();
131
+ vi.mocked(completeSimple).mockImplementation(
132
+ async (_model, _context, options) =>
133
+ new Promise((_, reject) => {
134
+ options?.signal?.addEventListener("abort", () => reject(new Error("aborted")));
135
+ }),
136
+ );
137
+
138
+ const promise = generateShortLabel(
139
+ {
140
+ model,
141
+ modelRegistry: {
142
+ getApiKeyAndHeaders: async () => ({ ok: true, apiKey: "secret" }),
143
+ },
144
+ },
145
+ {
146
+ systemPrompt: "system",
147
+ prompt: "prompt",
148
+ timeoutMs: 5,
149
+ },
150
+ );
151
+
152
+ await vi.advanceTimersByTimeAsync(5);
153
+ await expect(promise).resolves.toBe("");
154
+ });
155
+ });