@mrclrchtr/supi-extras 4.0.0 → 4.2.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 CHANGED
@@ -33,6 +33,7 @@ This package mixes a few commands and shortcuts with a few always-on UI tweaks.
33
33
  - `/exit` — exit pi
34
34
  - `/e` — alias for `/exit`
35
35
  - `/clear` — start a new session (alias for `/new`)
36
+ - `/clone-session <session-id>` — clone a session into the current worktree and switch to it; autocomplete searches IDs and session names
36
37
  - `/supi-stash` — browse, restore, copy, delete, or clear saved prompt drafts
37
38
 
38
39
  ## Shortcuts
@@ -83,6 +84,7 @@ That prevents git subprocesses from hanging while waiting for an interactive edi
83
84
  ## Source
84
85
 
85
86
  - `src/aliases.ts` — command aliases
87
+ - `src/clone-session.ts` — cross-worktree session cloning by ID
86
88
  - `src/prompt-stash.ts` — prompt stash shortcuts, persistence, and overlay
87
89
  - `src/skill-shortcut.ts` — `$skill-name` expansion and autocomplete
88
90
  - `src/tab-spinner.ts` — terminal tab-title spinner
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mrclrchtr/supi-core",
3
- "version": "4.0.0",
3
+ "version": "4.2.0",
4
4
  "description": "SuPi core — shared infrastructure for SuPi extensions (XML context tags, config system)",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -34,6 +34,7 @@ export function buildActionMenu(field: ScopedFieldValue, scope: SettingsScope):
34
34
  if (choices.length > 0) {
35
35
  for (const choice of choices) menu.push({ value: `set:${choice}`, label: choice });
36
36
  } else if (field.field.kind === "number") menu.push({ value: "edit", label: "Edit value…" });
37
+ else if (field.field.kind === "string") menu.push({ value: "edit", label: "Edit value…" });
37
38
  else if (field.field.kind === "stringList") menu.push({ value: "edit", label: "Edit values…" });
38
39
  else if (field.field.kind === "modelPicker")
39
40
  menu.push({ value: "edit", label: "Choose model…" });
@@ -86,6 +86,11 @@ export interface NumberField extends BaseField {
86
86
  values?: string[];
87
87
  }
88
88
 
89
+ /** One free-form string. */
90
+ export interface StringField extends BaseField {
91
+ kind: "string";
92
+ }
93
+
89
94
  /** Comma-separated string list. */
90
95
  export interface StringListField extends BaseField {
91
96
  kind: "stringList";
@@ -164,6 +169,7 @@ export type SettingsField =
164
169
  | BoolField
165
170
  | EnumField
166
171
  | NumberField
172
+ | StringField
167
173
  | StringListField
168
174
  | ModelPickerField
169
175
  | CustomField;
@@ -248,6 +254,8 @@ export function formatValue(value: unknown, field: SettingsField): string {
248
254
  return value ? "on" : "off";
249
255
  case "number":
250
256
  return String(value ?? "");
257
+ case "string":
258
+ return typeof value === "string" && value ? value : "none";
251
259
  case "stringList": {
252
260
  const arr = Array.isArray(value) ? value : [];
253
261
  return arr.length > 0 ? arr.map(String).join(", ") : "none";
@@ -271,6 +279,7 @@ export function sourceBadge(displayValue: string, source: ValueSource): string {
271
279
 
272
280
  /** Format the value used to prefill editors and compare concrete choices. */
273
281
  export function formatEditValue(value: unknown, field: SettingsField): string {
282
+ if (field.kind === "string") return typeof value === "string" ? value : "";
274
283
  if (field.kind === "stringList") {
275
284
  const arr = Array.isArray(value) ? value : [];
276
285
  return arr.map(String).join(", ");
@@ -26,6 +26,7 @@ export type {
26
26
  SettingsField,
27
27
  SettingsFieldAction,
28
28
  SettingsPersistedChange,
29
+ StringField,
29
30
  StringListField,
30
31
  ValueSource,
31
32
  } from "./settings/settings-schema.ts";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mrclrchtr/supi-extras",
3
- "version": "4.0.0",
3
+ "version": "4.2.0",
4
4
  "description": "SuPi extras — command aliases, skill shorthand, tab spinner, /supi-stash prompt stash with TUI overlay, and other small utilities",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -33,7 +33,7 @@
33
33
  ],
34
34
  "dependencies": {
35
35
  "clipboardy": "^5.3.1",
36
- "@mrclrchtr/supi-core": "4.0.0"
36
+ "@mrclrchtr/supi-core": "4.2.0"
37
37
  },
38
38
  "bundledDependencies": [
39
39
  "@mrclrchtr/supi-core"
@@ -0,0 +1,59 @@
1
+ import { type ExtensionAPI, SessionManager } from "@earendil-works/pi-coding-agent";
2
+
3
+ export default function cloneSession(pi: ExtensionAPI) {
4
+ let completionSessions: Awaited<ReturnType<typeof SessionManager.listAll>> | undefined;
5
+
6
+ pi.registerCommand("clone-session", {
7
+ description: "Clone a session by ID into this worktree and switch to it",
8
+ getArgumentCompletions: async (prefix) => {
9
+ completionSessions ??= await SessionManager.listAll();
10
+ const query = prefix.trim().toLowerCase();
11
+ const matches = completionSessions.filter(
12
+ (session) => session.id.startsWith(query) || session.name?.toLowerCase().includes(query),
13
+ );
14
+ return matches.length > 0
15
+ ? matches.map((session) => ({
16
+ value: session.id,
17
+ label: session.id,
18
+ description: [session.name, session.cwd].filter(Boolean).join(" — ") || undefined,
19
+ }))
20
+ : null;
21
+ },
22
+ handler: async (args, ctx) => {
23
+ const sessionId = args.trim();
24
+ if (!sessionId) {
25
+ ctx.ui.notify("Usage: /clone-session <session-id>", "warning");
26
+ return;
27
+ }
28
+
29
+ let sessionFile: string;
30
+ try {
31
+ const sourceSession = (await SessionManager.listAll()).find(
32
+ (session) => session.id === sessionId,
33
+ );
34
+ if (!sourceSession) {
35
+ ctx.ui.notify(`Session not found: ${sessionId}`, "warning");
36
+ return;
37
+ }
38
+
39
+ const clonedSessionFile = SessionManager.forkFrom(
40
+ sourceSession.path,
41
+ ctx.cwd,
42
+ ).getSessionFile();
43
+ if (!clonedSessionFile) throw new Error("cloned session was not persisted");
44
+ sessionFile = clonedSessionFile;
45
+ } catch (error) {
46
+ const message = error instanceof Error ? error.message : String(error);
47
+ ctx.ui.notify(`Could not clone session: ${message}`, "error");
48
+ return;
49
+ }
50
+
51
+ const result = await ctx.switchSession(sessionFile, {
52
+ withSession: async (nextCtx) => {
53
+ nextCtx.ui.notify("Session cloned into this worktree", "info");
54
+ },
55
+ });
56
+ if (result.cancelled) ctx.ui.notify(`Session cloned to ${sessionFile}`, "info");
57
+ },
58
+ });
59
+ }
package/src/index.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  import aliases from "./aliases.ts";
2
+ import cloneSession from "./clone-session.ts";
2
3
  import copyPrompt from "./copy-prompt.ts";
3
4
  import gitEditor from "./git-editor.ts";
4
5
  import promptStash from "./prompt-stash.ts";
@@ -19,6 +20,7 @@ export default function (pi: Parameters<typeof tabSpinner>[0]) {
19
20
  tabSpinner(pi);
20
21
  promptStash(pi);
21
22
  copyPrompt(pi);
23
+ cloneSession(pi);
22
24
  gitEditor(pi);
23
25
  aliases(pi);
24
26
  skillShortcut(pi);