my-pi 0.0.13 → 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/dist/{api-CWEizv2k.js → api-1ZXLxSgP.js} +223 -43
- package/dist/api-1ZXLxSgP.js.map +1 -0
- package/dist/api.js +1 -1
- package/dist/index.js +7 -1
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/extensions/config.test.ts +3 -0
- package/src/extensions/config.ts +12 -2
- package/src/extensions/handoff.ts +152 -66
- package/src/extensions/session-name.ts +234 -0
- package/dist/api-CWEizv2k.js.map +0 -1
package/dist/api.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import { n as create_my_pi, r as runPrintMode, t as InteractiveMode } from "./api-
|
|
1
|
+
import { n as create_my_pi, r as runPrintMode, t as InteractiveMode } from "./api-1ZXLxSgP.js";
|
|
2
2
|
export { InteractiveMode, create_my_pi, runPrintMode };
|
package/dist/index.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { n as create_my_pi } from "./api-
|
|
2
|
+
import { n as create_my_pi } from "./api-1ZXLxSgP.js";
|
|
3
3
|
import { InteractiveMode, runPrintMode } from "@mariozechner/pi-coding-agent";
|
|
4
4
|
import { defineCommand, runMain } from "citty";
|
|
5
5
|
import { readFileSync } from "node:fs";
|
|
@@ -106,6 +106,11 @@ runMain(defineCommand({
|
|
|
106
106
|
description: "Disable LSP extension",
|
|
107
107
|
default: false
|
|
108
108
|
},
|
|
109
|
+
"no-session-name": {
|
|
110
|
+
type: "boolean",
|
|
111
|
+
description: "Disable session name extension",
|
|
112
|
+
default: false
|
|
113
|
+
},
|
|
109
114
|
telemetry: {
|
|
110
115
|
type: "boolean",
|
|
111
116
|
description: "Enable local SQLite telemetry for this process",
|
|
@@ -179,6 +184,7 @@ runMain(defineCommand({
|
|
|
179
184
|
recall: !args["no-builtin"] && !args["no-recall"],
|
|
180
185
|
prompt_presets: !args["no-builtin"] && !args["no-prompt-presets"],
|
|
181
186
|
lsp: !args["no-builtin"] && !args["no-lsp"],
|
|
187
|
+
session_name: !args["no-builtin"] && !args["no-session-name"],
|
|
182
188
|
telemetry: telemetry_override,
|
|
183
189
|
telemetry_db_path: args["telemetry-db"],
|
|
184
190
|
model: args.model,
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","names":[],"sources":["../src/index.ts"],"sourcesContent":["#!/usr/bin/env node\n\n// CLI for my-pi — composable pi coding agent\n// Extension stacking patterns inspired by https://github.com/disler/pi-vs-claude-code\n\nimport {\n\tInteractiveMode,\n\trunPrintMode,\n} from '@mariozechner/pi-coding-agent';\nimport { defineCommand, runMain } from 'citty';\nimport { readFileSync } from 'node:fs';\nimport { dirname, join, resolve } from 'node:path';\nimport { fileURLToPath } from 'node:url';\nimport { create_my_pi } from './api.js';\n\n// Suppress node:sqlite ExperimentalWarning\nprocess.removeAllListeners('warning');\nprocess.on('warning', (warning) => {\n\tif (warning.name !== 'ExperimentalWarning') {\n\t\tconsole.warn(warning);\n\t}\n});\n\nconst __dirname = dirname(fileURLToPath(import.meta.url));\nconst pkg = JSON.parse(\n\treadFileSync(join(__dirname, '..', 'package.json'), 'utf-8'),\n);\n\n// citty can't handle repeatable args, so parse -e from argv directly\n// (citty uses strict: false, so unknown flags are silently ignored)\nfunction parse_extension_paths(argv: string[]): string[] {\n\tconst paths: string[] = [];\n\tfor (let i = 0; i < argv.length; i++) {\n\t\tif (\n\t\t\t(argv[i] === '-e' || argv[i] === '--extension') &&\n\t\t\ti + 1 < argv.length\n\t\t) {\n\t\t\tpaths.push(resolve(argv[++i]));\n\t\t}\n\t}\n\treturn paths;\n}\n\nasync function read_stdin(): Promise<string> {\n\tconst chunks: Buffer[] = [];\n\tfor await (const chunk of process.stdin) {\n\t\tchunks.push(chunk as Buffer);\n\t}\n\treturn Buffer.concat(chunks).toString('utf-8').trim();\n}\n\nfunction print_usage(): void {\n\tconsole.log(`my-pi v${pkg.version} — composable pi coding agent\\n`);\n\tconsole.log('Usage:');\n\tconsole.log(\n\t\t' my-pi \"prompt\" One-shot print mode',\n\t);\n\tconsole.log(\n\t\t' my-pi Interactive TUI mode',\n\t);\n\tconsole.log(\n\t\t' my-pi -P \"prompt\" Explicit print mode',\n\t);\n\tconsole.log(\n\t\t' my-pi --json \"prompt\" NDJSON output for agents',\n\t);\n\tconsole.log(\n\t\t' my-pi -e ext.ts Stack an extension',\n\t);\n\tconsole.log(\n\t\t' my-pi -e a.ts -e b.ts Stack multiple extensions',\n\t);\n\tconsole.log(\n\t\t' my-pi --telemetry --json \"task\" Enable local SQLite telemetry',\n\t);\n\tconsole.log(\n\t\t' my-pi --agent-dir /tmp/pi-agent Override auth/config/session dir',\n\t);\n\tconsole.log(\n\t\t' echo \"prompt\" | my-pi --json Pipe stdin as prompt',\n\t);\n\tconsole.log(\n\t\t' my-pi -m claude-haiku-4-5-20241022 Set initial model',\n\t);\n\tconsole.log(\n\t\t' my-pi --no-builtin -e ext.ts Skip all built-in extensions',\n\t);\n}\n\nconst main = defineCommand({\n\tmeta: {\n\t\tname: 'my-pi',\n\t\tversion: pkg.version,\n\t\tdescription:\n\t\t\t'Composable pi coding agent with MCP, LSP, chains, presets, and local eval telemetry',\n\t},\n\targs: {\n\t\tprint: {\n\t\t\ttype: 'boolean',\n\t\t\talias: 'P',\n\t\t\tdescription: 'Print mode (non-interactive, one-shot)',\n\t\t\tdefault: false,\n\t\t},\n\t\t'agent-dir': {\n\t\t\ttype: 'string',\n\t\t\tdescription:\n\t\t\t\t'Override Pi auth/config/session directory for this process',\n\t\t\trequired: false,\n\t\t},\n\t\tjson: {\n\t\t\ttype: 'boolean',\n\t\t\talias: 'j',\n\t\t\tdescription: 'Output NDJSON events (for agent consumption)',\n\t\t\tdefault: false,\n\t\t},\n\t\t'no-builtin': {\n\t\t\ttype: 'boolean',\n\t\t\tdescription: 'Disable all built-in extensions',\n\t\t\tdefault: false,\n\t\t},\n\t\t'no-mcp': {\n\t\t\ttype: 'boolean',\n\t\t\tdescription: 'Disable built-in MCP extension',\n\t\t\tdefault: false,\n\t\t},\n\t\t'no-skills': {\n\t\t\ttype: 'boolean',\n\t\t\tdescription: 'Disable built-in skills extension',\n\t\t\tdefault: false,\n\t\t},\n\t\t'no-chain': {\n\t\t\ttype: 'boolean',\n\t\t\tdescription: 'Disable built-in chain extension',\n\t\t\tdefault: false,\n\t\t},\n\t\t'no-filter': {\n\t\t\ttype: 'boolean',\n\t\t\tdescription: 'Disable secret redaction in tool output',\n\t\t\tdefault: false,\n\t\t},\n\t\t'no-handoff': {\n\t\t\ttype: 'boolean',\n\t\t\tdescription: 'Disable handoff extension',\n\t\t\tdefault: false,\n\t\t},\n\t\t'no-recall': {\n\t\t\ttype: 'boolean',\n\t\t\tdescription: 'Disable recall extension',\n\t\t\tdefault: false,\n\t\t},\n\t\t'no-prompt-presets': {\n\t\t\ttype: 'boolean',\n\t\t\tdescription: 'Disable prompt presets extension',\n\t\t\tdefault: false,\n\t\t},\n\t\t'no-lsp': {\n\t\t\ttype: 'boolean',\n\t\t\tdescription: 'Disable LSP extension',\n\t\t\tdefault: false,\n\t\t},\n\t\ttelemetry: {\n\t\t\ttype: 'boolean',\n\t\t\tdescription: 'Enable local SQLite telemetry for this process',\n\t\t\tdefault: false,\n\t\t},\n\t\t'no-telemetry': {\n\t\t\ttype: 'boolean',\n\t\t\tdescription: 'Disable local SQLite telemetry for this process',\n\t\t\tdefault: false,\n\t\t},\n\t\t'telemetry-db': {\n\t\t\ttype: 'string',\n\t\t\tdescription:\n\t\t\t\t'Override telemetry database path for this process',\n\t\t\trequired: false,\n\t\t},\n\t\tmodel: {\n\t\t\ttype: 'string',\n\t\t\talias: 'm',\n\t\t\tdescription:\n\t\t\t\t'Model to use (e.g. claude-sonnet-4-5-20241022, gpt-5.4)',\n\t\t},\n\t\t'system-prompt': {\n\t\t\ttype: 'string',\n\t\t\tdescription: 'Replace the base system prompt',\n\t\t\trequired: false,\n\t\t},\n\t\t'append-system-prompt': {\n\t\t\ttype: 'string',\n\t\t\tdescription: 'Append one-off instructions to the system prompt',\n\t\t\trequired: false,\n\t\t},\n\t\tprompt: {\n\t\t\ttype: 'string',\n\t\t\talias: 'p',\n\t\t\tdescription: 'Prompt text (alternative to positional argument)',\n\t\t\trequired: false,\n\t\t},\n\t},\n\tasync run({ args }) {\n\t\tconst cwd = process.cwd();\n\t\tconst extension_paths = parse_extension_paths(process.argv);\n\n\t\t// Resolve prompt: named --prompt flag > positional > stdin\n\t\tlet prompt = args.prompt;\n\t\tif (!prompt) {\n\t\t\t// Check for positional arguments (after citty strips flags)\n\t\t\tconst positionals = (args as any)._ as string[] | undefined;\n\t\t\tif (positionals && positionals.length > 0) {\n\t\t\t\tprompt = positionals[0];\n\t\t\t}\n\t\t}\n\t\tif (!prompt && !process.stdin.isTTY) {\n\t\t\tprompt = await read_stdin();\n\t\t}\n\n\t\t// Model validation (issue #5)\n\t\tif (args.model && /[/\\\\]/.test(args.model)) {\n\t\t\tconsole.error(\n\t\t\t\t`Error: Invalid model \"${args.model}\". Use bare model names without provider prefixes.`,\n\t\t\t);\n\t\t\tconsole.error(\n\t\t\t\t` Examples: claude-sonnet-4-5-20241022, gpt-5.4, mistral-large`,\n\t\t\t);\n\t\t\tprocess.exit(1);\n\t\t}\n\n\t\tif (\n\t\t\t!args.print &&\n\t\t\t!args.json &&\n\t\t\t!prompt &&\n\t\t\t!process.stdout.isTTY\n\t\t) {\n\t\t\tprint_usage();\n\t\t\treturn;\n\t\t}\n\n\t\t// Startup feedback so silence = broken (issue #3)\n\t\tif (args.print || args.json || prompt) {\n\t\t\tprocess.stderr.write(\n\t\t\t\t`my-pi: connecting to ${args.model || 'default model'}...\\n`,\n\t\t\t);\n\t\t}\n\n\t\tif (args.telemetry && args['no-telemetry']) {\n\t\t\tconsole.error(\n\t\t\t\t'Error: --telemetry and --no-telemetry cannot be used together.',\n\t\t\t);\n\t\t\tprocess.exit(1);\n\t\t}\n\n\t\tconst telemetry_override = args.telemetry\n\t\t\t? true\n\t\t\t: args['no-telemetry']\n\t\t\t\t? false\n\t\t\t\t: undefined;\n\n\t\tconst runtime = await create_my_pi({\n\t\t\tcwd,\n\t\t\tagent_dir: args['agent-dir'],\n\t\t\textensions: extension_paths,\n\t\t\tmcp: !args['no-builtin'] && !args['no-mcp'],\n\t\t\tskills: !args['no-builtin'] && !args['no-skills'],\n\t\t\tchain: !args['no-builtin'] && !args['no-chain'],\n\t\t\tfilter_output: !args['no-builtin'] && !args['no-filter'],\n\t\t\thandoff: !args['no-builtin'] && !args['no-handoff'],\n\t\t\trecall: !args['no-builtin'] && !args['no-recall'],\n\t\t\tprompt_presets:\n\t\t\t\t!args['no-builtin'] && !args['no-prompt-presets'],\n\t\t\tlsp: !args['no-builtin'] && !args['no-lsp'],\n\t\t\ttelemetry: telemetry_override,\n\t\t\ttelemetry_db_path: args['telemetry-db'],\n\t\t\tmodel: args.model,\n\t\t\tsystem_prompt: args['system-prompt'],\n\t\t\tappend_system_prompt: args['append-system-prompt'],\n\t\t});\n\n\t\tif (args.print || args.json || prompt) {\n\t\t\tconst code = await runPrintMode(runtime, {\n\t\t\t\tmode: args.json ? 'json' : 'text',\n\t\t\t\tinitialMessage: prompt || '',\n\t\t\t\tinitialImages: [],\n\t\t\t\tmessages: [],\n\t\t\t});\n\t\t\tprocess.exit(code);\n\t\t} else if (!process.stdout.isTTY) {\n\t\t\tprint_usage();\n\t\t} else {\n\t\t\tconst mode = new InteractiveMode(runtime, {\n\t\t\t\tmigratedProviders: [],\n\t\t\t\tmodelFallbackMessage: undefined,\n\t\t\t\tinitialMessage: undefined,\n\t\t\t\tinitialImages: [],\n\t\t\t\tinitialMessages: [],\n\t\t\t});\n\t\t\tawait mode.run();\n\t\t}\n\t},\n});\n\nvoid runMain(main);\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"index.js","names":[],"sources":["../src/index.ts"],"sourcesContent":["#!/usr/bin/env node\n\n// CLI for my-pi — composable pi coding agent\n// Extension stacking patterns inspired by https://github.com/disler/pi-vs-claude-code\n\nimport {\n\tInteractiveMode,\n\trunPrintMode,\n} from '@mariozechner/pi-coding-agent';\nimport { defineCommand, runMain } from 'citty';\nimport { readFileSync } from 'node:fs';\nimport { dirname, join, resolve } from 'node:path';\nimport { fileURLToPath } from 'node:url';\nimport { create_my_pi } from './api.js';\n\n// Suppress node:sqlite ExperimentalWarning\nprocess.removeAllListeners('warning');\nprocess.on('warning', (warning) => {\n\tif (warning.name !== 'ExperimentalWarning') {\n\t\tconsole.warn(warning);\n\t}\n});\n\nconst __dirname = dirname(fileURLToPath(import.meta.url));\nconst pkg = JSON.parse(\n\treadFileSync(join(__dirname, '..', 'package.json'), 'utf-8'),\n);\n\n// citty can't handle repeatable args, so parse -e from argv directly\n// (citty uses strict: false, so unknown flags are silently ignored)\nfunction parse_extension_paths(argv: string[]): string[] {\n\tconst paths: string[] = [];\n\tfor (let i = 0; i < argv.length; i++) {\n\t\tif (\n\t\t\t(argv[i] === '-e' || argv[i] === '--extension') &&\n\t\t\ti + 1 < argv.length\n\t\t) {\n\t\t\tpaths.push(resolve(argv[++i]));\n\t\t}\n\t}\n\treturn paths;\n}\n\nasync function read_stdin(): Promise<string> {\n\tconst chunks: Buffer[] = [];\n\tfor await (const chunk of process.stdin) {\n\t\tchunks.push(chunk as Buffer);\n\t}\n\treturn Buffer.concat(chunks).toString('utf-8').trim();\n}\n\nfunction print_usage(): void {\n\tconsole.log(`my-pi v${pkg.version} — composable pi coding agent\\n`);\n\tconsole.log('Usage:');\n\tconsole.log(\n\t\t' my-pi \"prompt\" One-shot print mode',\n\t);\n\tconsole.log(\n\t\t' my-pi Interactive TUI mode',\n\t);\n\tconsole.log(\n\t\t' my-pi -P \"prompt\" Explicit print mode',\n\t);\n\tconsole.log(\n\t\t' my-pi --json \"prompt\" NDJSON output for agents',\n\t);\n\tconsole.log(\n\t\t' my-pi -e ext.ts Stack an extension',\n\t);\n\tconsole.log(\n\t\t' my-pi -e a.ts -e b.ts Stack multiple extensions',\n\t);\n\tconsole.log(\n\t\t' my-pi --telemetry --json \"task\" Enable local SQLite telemetry',\n\t);\n\tconsole.log(\n\t\t' my-pi --agent-dir /tmp/pi-agent Override auth/config/session dir',\n\t);\n\tconsole.log(\n\t\t' echo \"prompt\" | my-pi --json Pipe stdin as prompt',\n\t);\n\tconsole.log(\n\t\t' my-pi -m claude-haiku-4-5-20241022 Set initial model',\n\t);\n\tconsole.log(\n\t\t' my-pi --no-builtin -e ext.ts Skip all built-in extensions',\n\t);\n}\n\nconst main = defineCommand({\n\tmeta: {\n\t\tname: 'my-pi',\n\t\tversion: pkg.version,\n\t\tdescription:\n\t\t\t'Composable pi coding agent with MCP, LSP, chains, presets, and local eval telemetry',\n\t},\n\targs: {\n\t\tprint: {\n\t\t\ttype: 'boolean',\n\t\t\talias: 'P',\n\t\t\tdescription: 'Print mode (non-interactive, one-shot)',\n\t\t\tdefault: false,\n\t\t},\n\t\t'agent-dir': {\n\t\t\ttype: 'string',\n\t\t\tdescription:\n\t\t\t\t'Override Pi auth/config/session directory for this process',\n\t\t\trequired: false,\n\t\t},\n\t\tjson: {\n\t\t\ttype: 'boolean',\n\t\t\talias: 'j',\n\t\t\tdescription: 'Output NDJSON events (for agent consumption)',\n\t\t\tdefault: false,\n\t\t},\n\t\t'no-builtin': {\n\t\t\ttype: 'boolean',\n\t\t\tdescription: 'Disable all built-in extensions',\n\t\t\tdefault: false,\n\t\t},\n\t\t'no-mcp': {\n\t\t\ttype: 'boolean',\n\t\t\tdescription: 'Disable built-in MCP extension',\n\t\t\tdefault: false,\n\t\t},\n\t\t'no-skills': {\n\t\t\ttype: 'boolean',\n\t\t\tdescription: 'Disable built-in skills extension',\n\t\t\tdefault: false,\n\t\t},\n\t\t'no-chain': {\n\t\t\ttype: 'boolean',\n\t\t\tdescription: 'Disable built-in chain extension',\n\t\t\tdefault: false,\n\t\t},\n\t\t'no-filter': {\n\t\t\ttype: 'boolean',\n\t\t\tdescription: 'Disable secret redaction in tool output',\n\t\t\tdefault: false,\n\t\t},\n\t\t'no-handoff': {\n\t\t\ttype: 'boolean',\n\t\t\tdescription: 'Disable handoff extension',\n\t\t\tdefault: false,\n\t\t},\n\t\t'no-recall': {\n\t\t\ttype: 'boolean',\n\t\t\tdescription: 'Disable recall extension',\n\t\t\tdefault: false,\n\t\t},\n\t\t'no-prompt-presets': {\n\t\t\ttype: 'boolean',\n\t\t\tdescription: 'Disable prompt presets extension',\n\t\t\tdefault: false,\n\t\t},\n\t\t'no-lsp': {\n\t\t\ttype: 'boolean',\n\t\t\tdescription: 'Disable LSP extension',\n\t\t\tdefault: false,\n\t\t},\n\t\t'no-session-name': {\n\t\t\ttype: 'boolean',\n\t\t\tdescription: 'Disable session name extension',\n\t\t\tdefault: false,\n\t\t},\n\t\ttelemetry: {\n\t\t\ttype: 'boolean',\n\t\t\tdescription: 'Enable local SQLite telemetry for this process',\n\t\t\tdefault: false,\n\t\t},\n\t\t'no-telemetry': {\n\t\t\ttype: 'boolean',\n\t\t\tdescription: 'Disable local SQLite telemetry for this process',\n\t\t\tdefault: false,\n\t\t},\n\t\t'telemetry-db': {\n\t\t\ttype: 'string',\n\t\t\tdescription:\n\t\t\t\t'Override telemetry database path for this process',\n\t\t\trequired: false,\n\t\t},\n\t\tmodel: {\n\t\t\ttype: 'string',\n\t\t\talias: 'm',\n\t\t\tdescription:\n\t\t\t\t'Model to use (e.g. claude-sonnet-4-5-20241022, gpt-5.4)',\n\t\t},\n\t\t'system-prompt': {\n\t\t\ttype: 'string',\n\t\t\tdescription: 'Replace the base system prompt',\n\t\t\trequired: false,\n\t\t},\n\t\t'append-system-prompt': {\n\t\t\ttype: 'string',\n\t\t\tdescription: 'Append one-off instructions to the system prompt',\n\t\t\trequired: false,\n\t\t},\n\t\tprompt: {\n\t\t\ttype: 'string',\n\t\t\talias: 'p',\n\t\t\tdescription: 'Prompt text (alternative to positional argument)',\n\t\t\trequired: false,\n\t\t},\n\t},\n\tasync run({ args }) {\n\t\tconst cwd = process.cwd();\n\t\tconst extension_paths = parse_extension_paths(process.argv);\n\n\t\t// Resolve prompt: named --prompt flag > positional > stdin\n\t\tlet prompt = args.prompt;\n\t\tif (!prompt) {\n\t\t\t// Check for positional arguments (after citty strips flags)\n\t\t\tconst positionals = (args as any)._ as string[] | undefined;\n\t\t\tif (positionals && positionals.length > 0) {\n\t\t\t\tprompt = positionals[0];\n\t\t\t}\n\t\t}\n\t\tif (!prompt && !process.stdin.isTTY) {\n\t\t\tprompt = await read_stdin();\n\t\t}\n\n\t\t// Model validation (issue #5)\n\t\tif (args.model && /[/\\\\]/.test(args.model)) {\n\t\t\tconsole.error(\n\t\t\t\t`Error: Invalid model \"${args.model}\". Use bare model names without provider prefixes.`,\n\t\t\t);\n\t\t\tconsole.error(\n\t\t\t\t` Examples: claude-sonnet-4-5-20241022, gpt-5.4, mistral-large`,\n\t\t\t);\n\t\t\tprocess.exit(1);\n\t\t}\n\n\t\tif (\n\t\t\t!args.print &&\n\t\t\t!args.json &&\n\t\t\t!prompt &&\n\t\t\t!process.stdout.isTTY\n\t\t) {\n\t\t\tprint_usage();\n\t\t\treturn;\n\t\t}\n\n\t\t// Startup feedback so silence = broken (issue #3)\n\t\tif (args.print || args.json || prompt) {\n\t\t\tprocess.stderr.write(\n\t\t\t\t`my-pi: connecting to ${args.model || 'default model'}...\\n`,\n\t\t\t);\n\t\t}\n\n\t\tif (args.telemetry && args['no-telemetry']) {\n\t\t\tconsole.error(\n\t\t\t\t'Error: --telemetry and --no-telemetry cannot be used together.',\n\t\t\t);\n\t\t\tprocess.exit(1);\n\t\t}\n\n\t\tconst telemetry_override = args.telemetry\n\t\t\t? true\n\t\t\t: args['no-telemetry']\n\t\t\t\t? false\n\t\t\t\t: undefined;\n\n\t\tconst runtime = await create_my_pi({\n\t\t\tcwd,\n\t\t\tagent_dir: args['agent-dir'],\n\t\t\textensions: extension_paths,\n\t\t\tmcp: !args['no-builtin'] && !args['no-mcp'],\n\t\t\tskills: !args['no-builtin'] && !args['no-skills'],\n\t\t\tchain: !args['no-builtin'] && !args['no-chain'],\n\t\t\tfilter_output: !args['no-builtin'] && !args['no-filter'],\n\t\t\thandoff: !args['no-builtin'] && !args['no-handoff'],\n\t\t\trecall: !args['no-builtin'] && !args['no-recall'],\n\t\t\tprompt_presets:\n\t\t\t\t!args['no-builtin'] && !args['no-prompt-presets'],\n\t\t\tlsp: !args['no-builtin'] && !args['no-lsp'],\n\t\t\tsession_name: !args['no-builtin'] && !args['no-session-name'],\n\t\t\ttelemetry: telemetry_override,\n\t\t\ttelemetry_db_path: args['telemetry-db'],\n\t\t\tmodel: args.model,\n\t\t\tsystem_prompt: args['system-prompt'],\n\t\t\tappend_system_prompt: args['append-system-prompt'],\n\t\t});\n\n\t\tif (args.print || args.json || prompt) {\n\t\t\tconst code = await runPrintMode(runtime, {\n\t\t\t\tmode: args.json ? 'json' : 'text',\n\t\t\t\tinitialMessage: prompt || '',\n\t\t\t\tinitialImages: [],\n\t\t\t\tmessages: [],\n\t\t\t});\n\t\t\tprocess.exit(code);\n\t\t} else if (!process.stdout.isTTY) {\n\t\t\tprint_usage();\n\t\t} else {\n\t\t\tconst mode = new InteractiveMode(runtime, {\n\t\t\t\tmigratedProviders: [],\n\t\t\t\tmodelFallbackMessage: undefined,\n\t\t\t\tinitialMessage: undefined,\n\t\t\t\tinitialImages: [],\n\t\t\t\tinitialMessages: [],\n\t\t\t});\n\t\t\tawait mode.run();\n\t\t}\n\t},\n});\n\nvoid runMain(main);\n"],"mappings":"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"}
|
package/package.json
CHANGED
package/src/extensions/config.ts
CHANGED
|
@@ -16,7 +16,8 @@ export type BuiltinExtensionKey =
|
|
|
16
16
|
| 'handoff'
|
|
17
17
|
| 'recall'
|
|
18
18
|
| 'prompt-presets'
|
|
19
|
-
| 'lsp'
|
|
19
|
+
| 'lsp'
|
|
20
|
+
| 'session-name';
|
|
20
21
|
|
|
21
22
|
export interface BuiltinExtensionInfo {
|
|
22
23
|
key: BuiltinExtensionKey;
|
|
@@ -79,7 +80,8 @@ export const BUILTIN_EXTENSIONS: BuiltinExtensionInfo[] = [
|
|
|
79
80
|
{
|
|
80
81
|
key: 'handoff',
|
|
81
82
|
label: 'Handoff',
|
|
82
|
-
description:
|
|
83
|
+
description:
|
|
84
|
+
'AI-generated session handoff with editor review and new-session prefill',
|
|
83
85
|
cli_flag: '--no-handoff',
|
|
84
86
|
aliases: ['handoff'],
|
|
85
87
|
},
|
|
@@ -106,6 +108,14 @@ export const BUILTIN_EXTENSIONS: BuiltinExtensionInfo[] = [
|
|
|
106
108
|
cli_flag: '--no-lsp',
|
|
107
109
|
aliases: ['lsp', 'language-server'],
|
|
108
110
|
},
|
|
111
|
+
{
|
|
112
|
+
key: 'session-name',
|
|
113
|
+
label: 'Session name',
|
|
114
|
+
description:
|
|
115
|
+
'AI-powered session auto-naming and /session-name command',
|
|
116
|
+
cli_flag: '--no-session-name',
|
|
117
|
+
aliases: ['session-name', 'session', 'auto-name'],
|
|
118
|
+
},
|
|
109
119
|
];
|
|
110
120
|
|
|
111
121
|
export function get_builtin_extensions_config_path(): string {
|
|
@@ -1,87 +1,173 @@
|
|
|
1
|
-
// Handoff extension —
|
|
2
|
-
// Inspired by jayshah5696/pi-agent-extensions
|
|
1
|
+
// Handoff extension — generate a focused prompt for a new session
|
|
3
2
|
|
|
4
|
-
import type
|
|
5
|
-
import {
|
|
6
|
-
|
|
3
|
+
import { complete, type Message } from '@mariozechner/pi-ai';
|
|
4
|
+
import type {
|
|
5
|
+
ExtensionAPI,
|
|
6
|
+
SessionEntry,
|
|
7
|
+
} from '@mariozechner/pi-coding-agent';
|
|
8
|
+
import {
|
|
9
|
+
BorderedLoader,
|
|
10
|
+
convertToLlm,
|
|
11
|
+
serializeConversation,
|
|
12
|
+
} from '@mariozechner/pi-coding-agent';
|
|
7
13
|
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
.join('\n');
|
|
30
|
-
|
|
31
|
-
if (!text) return;
|
|
32
|
-
|
|
33
|
-
const summary =
|
|
34
|
-
text.length > 200 ? text.slice(0, 200) + '...' : text;
|
|
35
|
-
|
|
36
|
-
history.push({
|
|
37
|
-
role: (msg.role as string) || 'unknown',
|
|
38
|
-
summary,
|
|
39
|
-
timestamp: Date.now(),
|
|
40
|
-
});
|
|
41
|
-
});
|
|
14
|
+
const SYSTEM_PROMPT = `You are a context transfer assistant. Given a conversation history and the user's goal for a new thread, generate a focused prompt that:
|
|
15
|
+
|
|
16
|
+
1. Summarizes relevant context from the conversation (decisions made, approaches taken, key findings)
|
|
17
|
+
2. Lists any relevant files that were discussed or modified
|
|
18
|
+
3. Clearly states the next task based on the user's goal
|
|
19
|
+
4. Is self-contained - the new thread should be able to proceed without the old conversation
|
|
20
|
+
|
|
21
|
+
Format your response as a prompt the user can send to start the new thread. Be concise but include all necessary context. Do not include any preamble like "Here's the prompt" - just output the prompt itself.
|
|
22
|
+
|
|
23
|
+
Example output format:
|
|
24
|
+
## Context
|
|
25
|
+
We've been working on X. Key decisions:
|
|
26
|
+
- Decision 1
|
|
27
|
+
- Decision 2
|
|
28
|
+
|
|
29
|
+
Files involved:
|
|
30
|
+
- path/to/file1.ts
|
|
31
|
+
- path/to/file2.ts
|
|
32
|
+
|
|
33
|
+
## Task
|
|
34
|
+
[Clear description of what to do next based on user's goal]`;
|
|
42
35
|
|
|
36
|
+
export default async function handoff(pi: ExtensionAPI) {
|
|
43
37
|
pi.registerCommand('handoff', {
|
|
44
38
|
description:
|
|
45
|
-
'
|
|
39
|
+
'Transfer context to a new focused session with an AI-generated prompt',
|
|
46
40
|
handler: async (args, ctx) => {
|
|
47
|
-
|
|
41
|
+
if (!ctx.hasUI) {
|
|
42
|
+
ctx.ui.notify('handoff requires interactive mode', 'error');
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
if (!ctx.model) {
|
|
47
|
+
ctx.ui.notify('No model selected', 'error');
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
48
50
|
|
|
49
|
-
|
|
51
|
+
const goal = args.trim();
|
|
52
|
+
if (!goal) {
|
|
50
53
|
ctx.ui.notify(
|
|
51
|
-
'
|
|
52
|
-
'
|
|
54
|
+
'Usage: /handoff <goal for new thread>',
|
|
55
|
+
'error',
|
|
53
56
|
);
|
|
54
57
|
return;
|
|
55
58
|
}
|
|
56
59
|
|
|
57
|
-
const
|
|
58
|
-
|
|
59
|
-
.
|
|
60
|
+
const branch = ctx.sessionManager.getBranch();
|
|
61
|
+
const messages = branch
|
|
62
|
+
.filter(
|
|
63
|
+
(entry): entry is SessionEntry & { type: 'message' } =>
|
|
64
|
+
entry.type === 'message',
|
|
65
|
+
)
|
|
66
|
+
.map((entry) => entry.message);
|
|
60
67
|
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
68
|
+
if (messages.length === 0) {
|
|
69
|
+
ctx.ui.notify('No conversation to hand off', 'error');
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
65
72
|
|
|
66
|
-
|
|
73
|
+
const llm_messages = convertToLlm(messages);
|
|
74
|
+
const conversation_text = serializeConversation(llm_messages);
|
|
75
|
+
const current_session_file =
|
|
76
|
+
ctx.sessionManager.getSessionFile();
|
|
77
|
+
const model = ctx.model;
|
|
78
|
+
|
|
79
|
+
const result = await ctx.ui.custom<string | null>(
|
|
80
|
+
(tui, theme, _kb, done) => {
|
|
81
|
+
const loader = new BorderedLoader(
|
|
82
|
+
tui,
|
|
83
|
+
theme,
|
|
84
|
+
'Generating handoff prompt...',
|
|
85
|
+
);
|
|
86
|
+
loader.onAbort = () => done(null);
|
|
87
|
+
|
|
88
|
+
const generate = async () => {
|
|
89
|
+
const auth =
|
|
90
|
+
await ctx.modelRegistry.getApiKeyAndHeaders(model);
|
|
91
|
+
if (!auth.ok || !auth.apiKey) {
|
|
92
|
+
throw new Error(
|
|
93
|
+
auth.ok
|
|
94
|
+
? `No API key for ${model.provider}`
|
|
95
|
+
: auth.error,
|
|
96
|
+
);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const user_message: Message = {
|
|
100
|
+
role: 'user',
|
|
101
|
+
content: [
|
|
102
|
+
{
|
|
103
|
+
type: 'text',
|
|
104
|
+
text: `## Conversation History\n\n${conversation_text}\n\n## User's Goal for New Thread\n\n${goal}`,
|
|
105
|
+
},
|
|
106
|
+
],
|
|
107
|
+
timestamp: Date.now(),
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
const response = await complete(
|
|
111
|
+
model,
|
|
112
|
+
{
|
|
113
|
+
systemPrompt: SYSTEM_PROMPT,
|
|
114
|
+
messages: [user_message],
|
|
115
|
+
},
|
|
116
|
+
{
|
|
117
|
+
apiKey: auth.apiKey,
|
|
118
|
+
headers: auth.headers,
|
|
119
|
+
signal: loader.signal,
|
|
120
|
+
},
|
|
121
|
+
);
|
|
122
|
+
|
|
123
|
+
if (response.stopReason === 'aborted') {
|
|
124
|
+
return null;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
return response.content
|
|
128
|
+
.filter(
|
|
129
|
+
(c): c is { type: 'text'; text: string } =>
|
|
130
|
+
c.type === 'text',
|
|
131
|
+
)
|
|
132
|
+
.map((c) => c.text)
|
|
133
|
+
.join('\n');
|
|
134
|
+
};
|
|
135
|
+
|
|
136
|
+
generate()
|
|
137
|
+
.then(done)
|
|
138
|
+
.catch((err) => {
|
|
139
|
+
console.error('Handoff generation failed:', err);
|
|
140
|
+
done(null);
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
return loader;
|
|
144
|
+
},
|
|
145
|
+
);
|
|
67
146
|
|
|
68
|
-
|
|
69
|
-
|
|
147
|
+
if (result === null) {
|
|
148
|
+
ctx.ui.notify('Cancelled', 'info');
|
|
149
|
+
return;
|
|
150
|
+
}
|
|
70
151
|
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
152
|
+
const edited_prompt = await ctx.ui.editor(
|
|
153
|
+
'Edit handoff prompt',
|
|
154
|
+
result,
|
|
155
|
+
);
|
|
156
|
+
if (edited_prompt === undefined) {
|
|
157
|
+
ctx.ui.notify('Cancelled', 'info');
|
|
158
|
+
return;
|
|
159
|
+
}
|
|
76
160
|
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
161
|
+
const new_session_result = await ctx.newSession({
|
|
162
|
+
parentSession: current_session_file,
|
|
163
|
+
});
|
|
164
|
+
if (new_session_result.cancelled) {
|
|
165
|
+
ctx.ui.notify('New session cancelled', 'info');
|
|
166
|
+
return;
|
|
167
|
+
}
|
|
81
168
|
|
|
82
|
-
ctx.ui.
|
|
83
|
-
|
|
84
|
-
);
|
|
169
|
+
ctx.ui.setEditorText(edited_prompt);
|
|
170
|
+
ctx.ui.notify('Handoff ready. Submit when ready.', 'info');
|
|
85
171
|
},
|
|
86
172
|
});
|
|
87
173
|
}
|
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
// Session name — AI-powered session naming
|
|
2
|
+
// Adapted from Thomas Lopes' pi dotfiles
|
|
3
|
+
|
|
4
|
+
import { complete, type Message } from '@mariozechner/pi-ai';
|
|
5
|
+
import type {
|
|
6
|
+
ExtensionAPI,
|
|
7
|
+
SessionEntry,
|
|
8
|
+
} from '@mariozechner/pi-coding-agent';
|
|
9
|
+
import {
|
|
10
|
+
BorderedLoader,
|
|
11
|
+
convertToLlm,
|
|
12
|
+
serializeConversation,
|
|
13
|
+
} from '@mariozechner/pi-coding-agent';
|
|
14
|
+
|
|
15
|
+
const SYSTEM_PROMPT = `You are a session naming assistant. Given a conversation history, generate a short, descriptive session name (2-5 words) that captures the main topic or task.
|
|
16
|
+
|
|
17
|
+
Guidelines:
|
|
18
|
+
- Be concise but specific
|
|
19
|
+
- Use kebab-case or natural language
|
|
20
|
+
- Focus on the core task/question
|
|
21
|
+
- Avoid generic names like "discussion" or "conversation"
|
|
22
|
+
- No quotes, no punctuation at the end
|
|
23
|
+
|
|
24
|
+
Examples:
|
|
25
|
+
- "fix auth bug" -> "fix-auth-bug" or "authentication fix"
|
|
26
|
+
- "how do I deploy to vercel" -> "vercel deployment"
|
|
27
|
+
- "explain react hooks" -> "react hooks explanation"
|
|
28
|
+
- "optimize database queries" -> "db query optimization"
|
|
29
|
+
|
|
30
|
+
Output ONLY the session name, nothing else.`;
|
|
31
|
+
|
|
32
|
+
const AUTO_NAME_THRESHOLD = 1;
|
|
33
|
+
const MAX_CHARS = 4000;
|
|
34
|
+
const MAX_NAME_LEN = 50;
|
|
35
|
+
|
|
36
|
+
function clean_name(value: string): string {
|
|
37
|
+
return value
|
|
38
|
+
.replace(/^["']|["']$/g, '')
|
|
39
|
+
.replace(/\n/g, ' ')
|
|
40
|
+
.replace(/\s+/g, ' ')
|
|
41
|
+
.trim()
|
|
42
|
+
.slice(0, MAX_NAME_LEN);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function truncate_conversation(value: string): string {
|
|
46
|
+
return value.length > MAX_CHARS
|
|
47
|
+
? value.slice(0, MAX_CHARS) + '\n...'
|
|
48
|
+
: value;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
async function generate_session_name(
|
|
52
|
+
ctx: {
|
|
53
|
+
modelRegistry: {
|
|
54
|
+
getApiKeyAndHeaders: (
|
|
55
|
+
model: NonNullable<
|
|
56
|
+
Parameters<
|
|
57
|
+
Parameters<ExtensionAPI['registerCommand']>[1]['handler']
|
|
58
|
+
>[1]['model']
|
|
59
|
+
>,
|
|
60
|
+
) => Promise<any>;
|
|
61
|
+
};
|
|
62
|
+
},
|
|
63
|
+
model: NonNullable<
|
|
64
|
+
Parameters<
|
|
65
|
+
Parameters<ExtensionAPI['registerCommand']>[1]['handler']
|
|
66
|
+
>[1]['model']
|
|
67
|
+
>,
|
|
68
|
+
conversation_text: string,
|
|
69
|
+
signal?: AbortSignal,
|
|
70
|
+
): Promise<string | null> {
|
|
71
|
+
const auth = await ctx.modelRegistry.getApiKeyAndHeaders(model);
|
|
72
|
+
if (!auth.ok || !auth.apiKey) {
|
|
73
|
+
throw new Error(
|
|
74
|
+
auth.ok ? `No API key for ${model.provider}` : auth.error,
|
|
75
|
+
);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const user_message: Message = {
|
|
79
|
+
role: 'user',
|
|
80
|
+
content: [
|
|
81
|
+
{
|
|
82
|
+
type: 'text',
|
|
83
|
+
text: `## Conversation History\n\n${truncate_conversation(conversation_text)}\n\nGenerate a concise session name for this conversation.`,
|
|
84
|
+
},
|
|
85
|
+
],
|
|
86
|
+
timestamp: Date.now(),
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
const response = await complete(
|
|
90
|
+
model,
|
|
91
|
+
{ systemPrompt: SYSTEM_PROMPT, messages: [user_message] },
|
|
92
|
+
{ apiKey: auth.apiKey, headers: auth.headers, signal },
|
|
93
|
+
);
|
|
94
|
+
|
|
95
|
+
if (response.stopReason === 'aborted') {
|
|
96
|
+
return null;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
return clean_name(
|
|
100
|
+
response.content
|
|
101
|
+
.filter(
|
|
102
|
+
(c): c is { type: 'text'; text: string } => c.type === 'text',
|
|
103
|
+
)
|
|
104
|
+
.map((c) => c.text.trim())
|
|
105
|
+
.join(' '),
|
|
106
|
+
);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
export default async function session_name(pi: ExtensionAPI) {
|
|
110
|
+
let auto_named_attempted = false;
|
|
111
|
+
|
|
112
|
+
pi.on('agent_end', async (_event, ctx) => {
|
|
113
|
+
if (!ctx.hasUI || !ctx.model) return;
|
|
114
|
+
if (pi.getSessionName() || auto_named_attempted) return;
|
|
115
|
+
|
|
116
|
+
const branch = ctx.sessionManager.getBranch();
|
|
117
|
+
const user_messages = branch.filter(
|
|
118
|
+
(entry): entry is SessionEntry & { type: 'message' } =>
|
|
119
|
+
entry.type === 'message' && entry.message.role === 'user',
|
|
120
|
+
);
|
|
121
|
+
if (user_messages.length < AUTO_NAME_THRESHOLD) return;
|
|
122
|
+
|
|
123
|
+
auto_named_attempted = true;
|
|
124
|
+
const messages = branch
|
|
125
|
+
.filter(
|
|
126
|
+
(entry): entry is SessionEntry & { type: 'message' } =>
|
|
127
|
+
entry.type === 'message',
|
|
128
|
+
)
|
|
129
|
+
.map((entry) => entry.message);
|
|
130
|
+
if (messages.length === 0) return;
|
|
131
|
+
|
|
132
|
+
const conversation_text = serializeConversation(
|
|
133
|
+
convertToLlm(messages),
|
|
134
|
+
);
|
|
135
|
+
|
|
136
|
+
generate_session_name(ctx, ctx.model, conversation_text)
|
|
137
|
+
.then((name) => {
|
|
138
|
+
if (!name) return;
|
|
139
|
+
pi.setSessionName(name);
|
|
140
|
+
ctx.ui.notify(`Auto-named: ${name}`, 'info');
|
|
141
|
+
})
|
|
142
|
+
.catch((err) => {
|
|
143
|
+
console.error('Auto-naming failed:', err);
|
|
144
|
+
});
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
pi.on('session_start', async () => {
|
|
148
|
+
auto_named_attempted = false;
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
pi.registerCommand('session-name', {
|
|
152
|
+
description:
|
|
153
|
+
'Set, show, or auto-generate the current session name',
|
|
154
|
+
handler: async (args, ctx) => {
|
|
155
|
+
const trimmed = args.trim();
|
|
156
|
+
|
|
157
|
+
if (!trimmed) {
|
|
158
|
+
const current = pi.getSessionName();
|
|
159
|
+
ctx.ui.notify(
|
|
160
|
+
current ? `Session: ${current}` : 'No session name set',
|
|
161
|
+
'info',
|
|
162
|
+
);
|
|
163
|
+
return;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
if (trimmed === '--auto' || trimmed === '-a') {
|
|
167
|
+
if (!ctx.hasUI || !ctx.model) {
|
|
168
|
+
ctx.ui.notify(
|
|
169
|
+
'Auto-naming requires interactive mode and a selected model',
|
|
170
|
+
'error',
|
|
171
|
+
);
|
|
172
|
+
return;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
const branch = ctx.sessionManager.getBranch();
|
|
176
|
+
const messages = branch
|
|
177
|
+
.filter(
|
|
178
|
+
(entry): entry is SessionEntry & { type: 'message' } =>
|
|
179
|
+
entry.type === 'message',
|
|
180
|
+
)
|
|
181
|
+
.map((entry) => entry.message);
|
|
182
|
+
if (messages.length === 0) {
|
|
183
|
+
ctx.ui.notify('No conversation to analyze', 'error');
|
|
184
|
+
return;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
const conversation_text = serializeConversation(
|
|
188
|
+
convertToLlm(messages),
|
|
189
|
+
);
|
|
190
|
+
|
|
191
|
+
const result = await ctx.ui.custom<string | null>(
|
|
192
|
+
(tui, theme, _kb, done) => {
|
|
193
|
+
const loader = new BorderedLoader(
|
|
194
|
+
tui,
|
|
195
|
+
theme,
|
|
196
|
+
'Generating session name...',
|
|
197
|
+
);
|
|
198
|
+
loader.onAbort = () => done(null);
|
|
199
|
+
|
|
200
|
+
generate_session_name(
|
|
201
|
+
ctx,
|
|
202
|
+
ctx.model!,
|
|
203
|
+
conversation_text,
|
|
204
|
+
loader.signal,
|
|
205
|
+
)
|
|
206
|
+
.then(done)
|
|
207
|
+
.catch((err) => {
|
|
208
|
+
console.error('Auto-naming failed:', err);
|
|
209
|
+
done(null);
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
return loader;
|
|
213
|
+
},
|
|
214
|
+
);
|
|
215
|
+
|
|
216
|
+
if (result === null) {
|
|
217
|
+
ctx.ui.notify('Auto-naming cancelled', 'info');
|
|
218
|
+
return;
|
|
219
|
+
}
|
|
220
|
+
if (!result) {
|
|
221
|
+
ctx.ui.notify('Failed to generate name', 'error');
|
|
222
|
+
return;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
pi.setSessionName(result);
|
|
226
|
+
ctx.ui.notify(`Session named: ${result}`, 'info');
|
|
227
|
+
return;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
pi.setSessionName(clean_name(trimmed));
|
|
231
|
+
ctx.ui.notify(`Session named: ${clean_name(trimmed)}`, 'info');
|
|
232
|
+
},
|
|
233
|
+
});
|
|
234
|
+
}
|