pi-umbra 0.3.1 → 0.4.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.
Files changed (34) hide show
  1. package/README.md +3 -4
  2. package/node_modules/pi-umbra-copy-chat/README.md +4 -7
  3. package/node_modules/pi-umbra-copy-chat/extensions/umbra-copy-chat.ts +16 -54
  4. package/node_modules/pi-umbra-copy-chat/package.json +1 -1
  5. package/node_modules/pi-umbra-help/checks/umbra-help.check.ts +10 -1
  6. package/node_modules/pi-umbra-help/extensions/umbra-help.ts +31 -0
  7. package/node_modules/pi-umbra-help/package.json +1 -1
  8. package/node_modules/pi-umbra-inputbar/README.md +1 -1
  9. package/node_modules/pi-umbra-inputbar/extensions/umbra-inputbar.ts +1 -1
  10. package/node_modules/pi-umbra-inputbar/package.json +2 -2
  11. package/node_modules/pi-umbra-shimmer/package.json +1 -1
  12. package/node_modules/pi-umbra-shimmer/patch.mjs +23 -21
  13. package/node_modules/pi-umbra-skill-matcher/package.json +1 -1
  14. package/node_modules/pi-umbra-skill-matcher/patch.mjs +7 -0
  15. package/node_modules/pi-umbra-subagents/README.md +19 -4
  16. package/node_modules/pi-umbra-subagents/extensions/umbra-subagents/bar/bar-line.ts +9 -2
  17. package/node_modules/pi-umbra-subagents/extensions/umbra-subagents/bar.check.ts +10 -1
  18. package/node_modules/pi-umbra-subagents/extensions/umbra-subagents/fan/index.ts +50 -5
  19. package/node_modules/pi-umbra-subagents/extensions/umbra-subagents/fan/spec.ts +24 -14
  20. package/node_modules/pi-umbra-subagents/extensions/umbra-subagents/fan/store.check.ts +119 -4
  21. package/node_modules/pi-umbra-subagents/extensions/umbra-subagents/fan/store.ts +138 -58
  22. package/node_modules/pi-umbra-subagents/extensions/umbra-subagents/fan/worktree.ts +156 -0
  23. package/node_modules/pi-umbra-subagents/extensions/umbra-subagents/skills/delegate/state.ts +19 -0
  24. package/node_modules/pi-umbra-subagents/extensions/umbra-subagents/skills/fan/SKILL.md +25 -4
  25. package/node_modules/pi-umbra-subagents/package.json +2 -2
  26. package/node_modules/pi-umbra-theme/checks/umbra-background.check.ts +7 -1
  27. package/node_modules/pi-umbra-theme/extensions/umbra-background.ts +2 -1
  28. package/node_modules/pi-umbra-theme/package.json +3 -3
  29. package/package.json +9 -12
  30. package/node_modules/pi-umbra-rename/LICENSE +0 -21
  31. package/node_modules/pi-umbra-rename/README.md +0 -18
  32. package/node_modules/pi-umbra-rename/checks/umbra-rename.check.ts +0 -55
  33. package/node_modules/pi-umbra-rename/extensions/umbra-rename.ts +0 -22
  34. package/node_modules/pi-umbra-rename/package.json +0 -36
@@ -0,0 +1,156 @@
1
+ import { execFileSync } from "node:child_process";
2
+ import { copyFileSync, existsSync, mkdirSync, rmdirSync, rmSync } from "node:fs";
3
+ import { dirname, join } from "node:path";
4
+ import type { GitResult } from "../skills/delegate/state.ts";
5
+
6
+ // A `[write]` branch's own copy of the repo. It starts from what the user sees, uncommitted and
7
+ // untracked work included, taken once per run as one base commit, so the branch's own changes
8
+ // are exactly `base..branch` and can be applied onto the user's working tree without dragging
9
+ // the user's work along a second time. Nothing here merges: the main session asks the user
10
+ // first, every time.
11
+ //
12
+ // Commits are plumbing (write-tree, commit-tree, update-ref): no hook runs and nothing is
13
+ // signed, so nothing can stop on a prompt while pi's thread waits. Worktrees live under the git
14
+ // directory, where test runners, linters, watchers and `git clean` never look.
15
+ // ponytail: every call is synchronous on pi's thread, about 0.1 s per branch on a few hundred
16
+ // files. Move to async execFile if users run [write] in repos with tens of thousands of files.
17
+
18
+ export type Repo = { root: string; prefix: string; common: string };
19
+ export type Worktree = { root: string; path: string; cwd: string; branch: string; base: string };
20
+
21
+ // git's own reason, not node's "Command failed: <argv>" line above it.
22
+ const git = (cwd: string, args: string[], env?: Record<string, string>) => {
23
+ try {
24
+ return execFileSync("git", ["-C", cwd, ...args], {
25
+ encoding: "utf8",
26
+ stdio: ["ignore", "pipe", "pipe"],
27
+ windowsHide: true,
28
+ ...(env ? { env: { ...process.env, ...env } } : {}),
29
+ }).trim();
30
+ } catch (error) {
31
+ const stderr = (error as { stderr?: string }).stderr?.trim();
32
+ throw stderr ? new Error(stderr) : error;
33
+ }
34
+ };
35
+
36
+ const tryGit = (cwd: string, args: string[]) => {
37
+ try {
38
+ return git(cwd, args);
39
+ } catch {
40
+ return "";
41
+ }
42
+ };
43
+
44
+ /** The repo the session sits in, or undefined outside one. `prefix` is the session's folder
45
+ * inside it, as git spells it, so a symlinked or 8.3 cwd still lands in the right place. */
46
+ export const gitRepo = (cwd: string): Repo | undefined => {
47
+ const [common, root, prefix = ""] = tryGit(cwd, ["rev-parse", "--path-format=absolute", "--git-common-dir", "--show-toplevel", "--show-prefix"]).split("\n");
48
+ return common && root ? { root, prefix, common } : undefined;
49
+ };
50
+
51
+ // The user's identity when git has one. Without it commit-tree refuses, and these commits never
52
+ // reach the user's history anyway: a merge applies the diff, it does not merge the commit.
53
+ const identity = (cwd: string) =>
54
+ tryGit(cwd, ["var", "GIT_COMMITTER_IDENT"])
55
+ ? undefined
56
+ : { GIT_AUTHOR_NAME: "pi fan", GIT_AUTHOR_EMAIL: "fan@localhost", GIT_COMMITTER_NAME: "pi fan", GIT_COMMITTER_EMAIL: "fan@localhost" };
57
+
58
+ /** The user's tree as they see it, as one commit on top of HEAD, or HEAD itself when nothing is
59
+ * uncommitted. Built in a scratch index, so the user's own index, diff settings and hooks never
60
+ * come into it, symlinks stay symlinks and a nested repo stays a gitlink. */
61
+ export const snapshot = (repo: Repo): string => {
62
+ const head = tryGit(repo.root, ["rev-parse", "--verify", "-q", "HEAD"]);
63
+ const index = join(repo.common, `fan-index-${process.pid}`);
64
+ const own = git(repo.root, ["rev-parse", "--path-format=absolute", "--git-path", "index"]);
65
+ // Seeded from the real index, so `add -A` only rehashes what changed.
66
+ if (existsSync(own)) copyFileSync(own, index);
67
+ try {
68
+ const env = { GIT_INDEX_FILE: index };
69
+ try {
70
+ git(repo.root, ["add", "-A", "--ignore-errors"], env);
71
+ } catch (error) {
72
+ // A nested repo with no commit yet, or a file this user cannot read, has nothing
73
+ // git can record, so the branch starts without it. Anything else is a real failure.
74
+ if (!/unable to index file/.test((error as Error).message)) throw error;
75
+ }
76
+ const tree = git(repo.root, ["write-tree"], env);
77
+ if (head && tree === git(repo.root, ["rev-parse", `${head}^{tree}`])) return head;
78
+ return git(repo.root, ["commit-tree", tree, ...(head ? ["-p", head] : []), "-m", "fan: the uncommitted work the branches started from"], identity(repo.root));
79
+ } finally {
80
+ rmSync(index, { force: true });
81
+ }
82
+ };
83
+
84
+ /** A new branch at `base` in its own worktree. The session's folder is made when the checkout
85
+ * lacks it: a folder holding only ignored files, or nothing yet, is not in any commit. */
86
+ export const openWorktree = (repo: Repo, base: string, path: string, branch: string): Worktree => {
87
+ const worktree = { root: repo.root, path, cwd: join(path, repo.prefix), branch, base };
88
+ // A hooks path that does not exist: post-checkout would run here, on pi's thread. Outside the
89
+ // try: when add fails, the path or branch may be another run's, and is not this one's to drop.
90
+ git(repo.root, ["-c", `core.hooksPath=${join(repo.common, "fan-no-hooks")}`, "worktree", "add", "-q", "-b", branch, path, base]);
91
+ try {
92
+ mkdirSync(worktree.cwd, { recursive: true });
93
+ return worktree;
94
+ } catch (error) {
95
+ dropWorktree(worktree);
96
+ throw error;
97
+ }
98
+ };
99
+
100
+ const removeWorktree = ({ root, path }: Worktree) => {
101
+ try {
102
+ git(root, ["worktree", "remove", "--force", path]);
103
+ // The run's folder under fan-worktrees, once its last branch is gone.
104
+ rmdirSync(dirname(path));
105
+ } catch {
106
+ // Already gone, a sibling still there, or a file the branch left locked: the work is on
107
+ // the branch either way, and `git worktree prune` clears the rest.
108
+ }
109
+ };
110
+
111
+ /** The worktree and its branch, gone. For a branch that never ran or changed nothing. */
112
+ export const dropWorktree = (worktree: Worktree) => {
113
+ removeWorktree(worktree);
114
+ tryGit(worktree.root, ["branch", "-D", worktree.branch]);
115
+ };
116
+
117
+ /** "+12 −3 · 2 files" from git's "2 files changed, 12 insertions(+), 3 deletions(-)". */
118
+ export const compactStat = (shortstat: string) => {
119
+ const count = (pattern: RegExp) => Number(pattern.exec(shortstat)?.[1] ?? 0);
120
+ const files = count(/(\d+) files? changed/);
121
+ return files ? `+${count(/(\d+) insertions?/)} −${count(/(\d+) deletions?/)} · ${files} ${files === 1 ? "file" : "files"}` : "";
122
+ };
123
+
124
+ /** Commit what the branch left, drop the worktree, keep the branch. A branch that changed
125
+ * nothing is dropped whole, so only branches worth a question are left. */
126
+ export const closeWorktree = (worktree: Worktree, message: string): GitResult => {
127
+ const { root, path, branch, base } = worktree;
128
+ let stat: string;
129
+ try {
130
+ git(path, ["add", "-A"]);
131
+ const tree = git(path, ["write-tree"]);
132
+ // On top of the branch's HEAD, not base: a branch that committed on its own keeps that.
133
+ if (tree !== git(path, ["rev-parse", "HEAD^{tree}"])) {
134
+ const commit = git(path, ["commit-tree", tree, "-p", "HEAD", "-m", message], identity(path));
135
+ git(path, ["update-ref", `refs/heads/${branch}`, commit]);
136
+ }
137
+ stat = compactStat(git(root, ["diff", "--shortstat", base, branch]));
138
+ } catch (error) {
139
+ // The worktree stays, so nothing the branch wrote is lost.
140
+ return { root, branch, base, stat: "", error: (error as Error).message.split("\n")[0] };
141
+ }
142
+ if (stat) removeWorktree(worktree);
143
+ else dropWorktree(worktree);
144
+ return { root, branch, base, stat };
145
+ };
146
+
147
+ const quote = (text: string) => `'${text.replaceAll("'", `'\\''`)}'`;
148
+
149
+ /** The merge the main session offers. Run at the repo root, so no path outside the session's
150
+ * folder is skipped; a plumbing diff, so no diff setting changes the patch; binary-safe; a
151
+ * plain apply onto the working tree, so the user's own uncommitted edits stay where they are
152
+ * and nothing is staged. It applies whole or not at all, and the branch goes only after. */
153
+ export const mergeCommand = ({ root, base, branch }: GitResult) =>
154
+ `git -C ${quote(root)} diff-tree -p --binary ${base} ${branch} | git -C ${quote(root)} apply && git -C ${quote(root)} branch -D ${branch}`;
155
+
156
+ export const discardCommand = ({ root, branch }: GitResult) => `git -C ${quote(root)} branch -D ${branch}`;
@@ -98,8 +98,15 @@ export type BranchView = BranchState & {
98
98
  alive: boolean;
99
99
  /** Finished for any reason. The N in "N/M agents". */
100
100
  settled: boolean;
101
+ /** A `[write]` branch's git branch, from `state/<stem>.git`, written when its worktree
102
+ * closes. Absent on a read-only branch and on one still running. */
103
+ git?: GitResult;
101
104
  };
102
105
 
106
+ /** What a write branch left behind: its branch, the base it started from, and "+N −M · K files"
107
+ * ("" when it changed nothing), or why its work could not be committed. */
108
+ export type GitResult = { root: string; branch: string; base: string; stat: string; error?: string };
109
+
103
110
  export type RunState = {
104
111
  dir: string;
105
112
  name: string;
@@ -178,6 +185,15 @@ const latestRun = (cwd: string): { dir: string; run: Run } | undefined => {
178
185
  return undefined;
179
186
  };
180
187
 
188
+ const readGit = (path: string): GitResult | undefined => {
189
+ try {
190
+ // SAFETY: written only by fan/store.ts through JSON.stringify of a GitResult.
191
+ return JSON.parse(readFileSync(path, "utf8")) as GitResult;
192
+ } catch {
193
+ return undefined;
194
+ }
195
+ };
196
+
181
197
  const readStates = (stateDir: string): BranchState[] => {
182
198
  let files: string[];
183
199
  try {
@@ -256,6 +272,8 @@ export function readRun(cwd: string, now = Date.now()): RunState | undefined {
256
272
  const branches: BranchView[] = order(readStates(stateDir)).map(({ state, depth }) => {
257
273
  const stem = stemOf(state);
258
274
  const exit = readExit(join(stateDir, `${stem}.exit`));
275
+ // Written just before the exit, so there is nothing to read until the exit is there.
276
+ const git = exit === undefined ? undefined : readGit(join(stateDir, `${stem}.git`));
259
277
  // A seeded branch whose pid is still null has not booted yet: it is pending, not
260
278
  // finished, or every row would count as done the instant the run started. Pending is
261
279
  // only honest while the owner is still around to launch it — an abandoned run's later
@@ -274,6 +292,7 @@ export function readRun(cwd: string, now = Date.now()): RunState | undefined {
274
292
  timedOut: exit?.code === 124,
275
293
  alive,
276
294
  settled,
295
+ ...(git ? { git } : {}),
277
296
  };
278
297
  });
279
298
 
@@ -1,13 +1,13 @@
1
1
  ---
2
2
  name: fan
3
- description: Fan independent read-only work out to parallel pi branches and watch them live in the agent panel. Use whenever a task splits into 2+ lookups that do not need each other, or when the user writes "fan", "delegate", "subagent", or "in parallel".
3
+ description: Fan independent work out to parallel pi branches and watch them live in the agent tree. Branches are read-only unless marked [write]. Use whenever a task splits into 2+ lookups that do not need each other, or when the user writes "fan", "delegate", "subagent", or "in parallel".
4
4
  ---
5
5
 
6
6
  # Fan
7
7
 
8
8
  There is no subagent tool and nothing is added to this prompt. A branch is a
9
9
  separate `pi -p` process the fan extension spawns and owns; each one writes a
10
- small live state file, so the panel above the input box shows what every branch
10
+ small live state file, so the agent tree under the input box shows what every branch
11
11
  is doing while it does it, not only what it concluded.
12
12
 
13
13
  ## Run
@@ -35,13 +35,17 @@ proposal: Given the Map results, write the design.
35
35
  phase's answers.
36
36
  - `label@provider/model: task` picks a model for that branch. Without it a
37
37
  branch runs on this session's model.
38
+ - `label [write]: task` (or `label@model [write]: task`) lets that branch edit
39
+ files and run commands, in its own git worktree on a new `fan/<run>/<branch>`
40
+ branch. See Write branches below.
38
41
  - 2 to 6 branches per phase. Each task names the files or directory and says
39
42
  exactly what to return.
40
43
 
41
44
  ## Rules
42
45
 
43
- - Branches are read-only: `read`, `grep`, `find`, `ls`, nothing else. Work that
44
- edits files stays in this session.
46
+ - Branches are read-only by default: `read`, `grep`, `find`, `ls`, nothing else.
47
+ Work that edits files stays in this session, unless the user asked for
48
+ parallel implementation; then use `[write]`.
45
49
  - A branch has an empty context. Repeating what you already know in the task
46
50
  text is the only way it learns it.
47
51
  - One run at a time. A second block while a run is live is ignored.
@@ -50,3 +54,20 @@ proposal: Given the Map results, write the design.
50
54
  they had already written stays on disk. `.pi-out/` is gitignored scratch.
51
55
  - A branch gets 5 minutes and is then cut off, so one wedged branch cannot
52
56
  hold the run open. Keep each task small enough to finish in that time.
57
+
58
+ ## Write branches
59
+
60
+ - Use `[write]` only when the user wants work done in parallel and each branch's
61
+ change is independent. Most runs stay read-only.
62
+ - The worktree starts from the user's working tree, uncommitted work included,
63
+ and only works inside a git repository; outside one the run does not start.
64
+ - A write branch gets `edit`, `write` and `bash`. It must not push, publish or
65
+ commit; its changes are committed to its branch when it ends, and its row
66
+ shows `Changed +N −M · K files on fan/...`.
67
+ - Nothing is merged. When the results arrive, ask the user, one question per
68
+ branch, whether to merge it, and only then run the command the follow-up gives.
69
+ - A write branch cannot stop on a question: its worktree closes when it ends.
70
+ Put every decision it needs in its task.
71
+ - Every write branch starts from the same snapshot, taken when the run starts:
72
+ a later phase does not see an earlier phase's changes. Work that builds on
73
+ another branch's change waits for that change to be merged, in a new run.
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "pi-umbra-subagents",
3
- "version": "0.1.2",
4
- "description": "Parallel read-only pi branches with a live panel above the input box, the delegate skill that starts them, and /umb-loop, which resubmits a prompt on a count or a timer.",
3
+ "version": "0.2.0",
4
+ "description": "Parallel pi branches with a live agent tree under the input box, read-only or in their own git worktree, the delegate skill that starts them, and /umb-loop, which resubmits a prompt on a count or a timer.",
5
5
  "keywords": [
6
6
  "pi-package",
7
7
  "pi",
@@ -4,6 +4,7 @@
4
4
  //
5
5
  // bun run umbra-background.check.ts
6
6
  import assert from "node:assert";
7
+ import { TuiAltScreen, TuiMainScreen } from "@earendil-works/pi-tui";
7
8
  import { mkdtempSync, writeFileSync } from "node:fs";
8
9
  import { tmpdir } from "node:os";
9
10
  import { join } from "node:path";
@@ -46,7 +47,12 @@ const notices: string[] = [];
46
47
  // The terminal as the TUI sees it: it answers the OSC 11 query with this colour, or never.
47
48
  let answer: { r: number; g: number; b: number } | undefined;
48
49
  const setWidget = (_key: string, factory: (tui: unknown) => unknown) =>
49
- factory({ queryTerminalBackgroundColor: async () => answer });
50
+ factory({ queryTerminalColors: async () => ({ background: answer }) });
51
+ // The stand-in above is only as good as its method name: pi 1.0 renamed the old query, the check
52
+ // kept passing and pi crashed at start. So the name is checked against the real TUI too.
53
+ for (const screen of [TuiMainScreen, TuiAltScreen]) {
54
+ assert.equal(typeof screen.prototype.queryTerminalColors, "function", `${screen.name} still has queryTerminalColors`);
55
+ }
50
56
  // Real pi hands the extension a Theme with NO sourcePath; the file it was read from is only on
51
57
  // the list getAllThemes() returns, under the name `path`. The check used to invent a sourcePath
52
58
  // and then assert against its own invention, which is why it passed while nothing happened on
@@ -106,7 +106,8 @@ export default function umbraBackground(pi: ExtensionAPI) {
106
106
 
107
107
  const verify = async (ctx: ExtensionContext, hex: string) => {
108
108
  // No reply means a terminal that answers no queries: unknown, not ignored.
109
- const rgb = await tui?.queryTerminalBackgroundColor({ timeoutMs: 1000 });
109
+ // pi 1.0 asks for every theme colour at once; queryTerminalBackgroundColor is gone.
110
+ const rgb = (await tui?.queryTerminalColors({ timeoutMs: 1000 }))?.background;
110
111
  if (rgb === undefined || current !== hex) return;
111
112
  const status: BackgroundStatus = { wanted: hex, ignored: !sameColour(hex, rgb), howTo: backgroundHowTo(process.env, hex) };
112
113
  (globalThis as { __umbraBackground?: BackgroundStatus }).__umbraBackground = status;
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-umbra-theme",
3
- "version": "0.2.1",
3
+ "version": "0.3.0",
4
4
  "description": "pi's look in one package: seven umbra themes and the terminal background they set, a status footer, a gutter beside model text, every tool call drawn as a card with rich diffs, and a working line that names the running tool.",
5
5
  "keywords": [
6
6
  "pi-package",
@@ -40,8 +40,8 @@
40
40
  },
41
41
  "peerDependencies": {
42
42
  "@earendil-works/pi-ai": "*",
43
- "@earendil-works/pi-coding-agent": "*",
44
- "@earendil-works/pi-tui": "*"
43
+ "@earendil-works/pi-coding-agent": ">=1.0.0",
44
+ "@earendil-works/pi-tui": ">=1.0.0"
45
45
  },
46
46
  "dependencies": {
47
47
  "@shikijs/cli": "^4.0.2"
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "pi-umbra",
3
- "version": "0.3.1",
4
- "description": "Every small thing pi does not build in, in one install: seven themes and the chrome around them, the input bar, session rename, a shimmer through the working indicator, mid-prompt skill completion, ask, copy-chat, preview, help, and parallel read-only branches with a live panel.",
3
+ "version": "0.4.0",
4
+ "description": "Every small thing pi does not build in, in one install: seven themes and the chrome around them, the input bar, a shimmer through the working indicator, mid-prompt skill completion, ask, copy-chat, preview, help, and parallel pi branches (read-only, or editing in their own git worktree) with a live agent tree.",
5
5
  "keywords": [
6
6
  "pi-package",
7
7
  "pi-theme",
@@ -31,7 +31,6 @@
31
31
  "./node_modules/pi-umbra-help/extensions",
32
32
  "./node_modules/pi-umbra-inputbar/extensions",
33
33
  "./node_modules/pi-umbra-preview/extensions",
34
- "./node_modules/pi-umbra-rename/extensions",
35
34
  "./node_modules/pi-umbra-shimmer/extensions",
36
35
  "./node_modules/pi-umbra-skill-matcher/extensions",
37
36
  "./node_modules/pi-umbra-subagents/extensions/umbra-subagents.ts",
@@ -45,16 +44,15 @@
45
44
  ]
46
45
  },
47
46
  "dependencies": {
48
- "pi-umbra-theme": "0.2.1",
47
+ "pi-umbra-theme": "0.3.0",
49
48
  "pi-umbra-ask": "0.1.2",
50
- "pi-umbra-copy-chat": "0.1.2",
51
- "pi-umbra-help": "0.1.4",
52
- "pi-umbra-inputbar": "0.1.4",
49
+ "pi-umbra-copy-chat": "0.1.3",
50
+ "pi-umbra-help": "0.1.5",
51
+ "pi-umbra-inputbar": "0.1.5",
53
52
  "pi-umbra-preview": "0.1.3",
54
- "pi-umbra-rename": "0.1.3",
55
- "pi-umbra-shimmer": "0.1.5",
56
- "pi-umbra-skill-matcher": "0.1.4",
57
- "pi-umbra-subagents": "0.1.2"
53
+ "pi-umbra-shimmer": "0.1.6",
54
+ "pi-umbra-skill-matcher": "0.1.5",
55
+ "pi-umbra-subagents": "0.2.0"
58
56
  },
59
57
  "bundledDependencies": [
60
58
  "pi-umbra-theme",
@@ -63,7 +61,6 @@
63
61
  "pi-umbra-help",
64
62
  "pi-umbra-inputbar",
65
63
  "pi-umbra-preview",
66
- "pi-umbra-rename",
67
64
  "pi-umbra-shimmer",
68
65
  "pi-umbra-skill-matcher",
69
66
  "pi-umbra-subagents"
@@ -1,21 +0,0 @@
1
- MIT License
2
-
3
- Copyright (c) 2026 grkn
4
-
5
- Permission is hereby granted, free of charge, to any person obtaining a copy
6
- of this software and associated documentation files (the "Software"), to deal
7
- in the Software without restriction, including without limitation the rights
8
- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
- copies of the Software, and to permit persons to whom the Software is
10
- furnished to do so, subject to the following conditions:
11
-
12
- The above copyright notice and this permission notice shall be included in all
13
- copies or substantial portions of the Software.
14
-
15
- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
- SOFTWARE.
@@ -1,18 +0,0 @@
1
- # pi-umbra-rename
2
-
3
- `/umb-rename` sets the session name and the terminal title in one step. pi keeps them apart,
4
- so after `/name` the window tab still shows the old title.
5
-
6
- ```sh
7
- pi install npm:pi-umbra-rename
8
- ```
9
-
10
- | Command | Effect |
11
- |---|---|
12
- | `/umb-rename api refactor` | rename straight away |
13
- | `/umb-rename` | ask for the name first; an empty answer changes nothing |
14
-
15
- The session name shows in `/resume` and, with pi-umbra-inputbar, on the input border. The
16
- terminal title becomes `π - api refactor - <folder>`.
17
-
18
- MIT.
@@ -1,55 +0,0 @@
1
- // /umb-rename writes two names through two different APIs, and the terminal title is built from
2
- // a third value. A fake pi is enough to pin all three, because the command is the whole extension.
3
- //
4
- // bun run umbra-rename.check.ts
5
- import assert from "node:assert";
6
- import rename from "../extensions/umbra-rename.ts";
7
-
8
- type Handler = (args: string, ctx: unknown) => Promise<void>;
9
-
10
- const load = (sessionName?: string) => {
11
- const calls = { name: undefined as string | undefined, title: undefined as string | undefined, notice: "" };
12
- let handler: Handler | undefined;
13
- const pi = {
14
- registerCommand: (_name: string, options: { handler: Handler }) => void (handler = options.handler),
15
- getSessionName: () => sessionName,
16
- setSessionName: (value: string) => void (calls.name = value),
17
- };
18
- rename(pi as never);
19
- const ctx = {
20
- ui: {
21
- setTitle: (value: string) => void (calls.title = value),
22
- notify: (value: string) => void (calls.notice = value),
23
- input: async () => prompted,
24
- },
25
- sessionManager: { getCwd: () => "/home/me/projects/real-estate" },
26
- };
27
- return { calls, run: (args: string) => handler!(args, ctx) };
28
- };
29
-
30
- let prompted: string | undefined;
31
-
32
- // The argument form. The prefix goes on the terminal title only: pi already spells the session
33
- // name on the input bar, so a π in the name itself would show up three times in two places.
34
- const given = load();
35
- await given.run(" ledger ");
36
- assert.equal(given.calls.name, "ledger", "the name is trimmed and carries no prefix");
37
- assert.equal(given.calls.title, "π - ledger - real-estate", "the title carries the prefix and the cwd");
38
- assert.equal(given.calls.notice, "renamed: ledger");
39
-
40
- // No argument: pi asks, and the current name is the prefill.
41
- prompted = "ledger";
42
- const asked = load("old-name");
43
- await asked.run("");
44
- assert.equal(asked.calls.name, "ledger", "the answer to the prompt is the new name");
45
-
46
- // Nothing typed, or the prompt dismissed, must leave both names alone rather than blanking them.
47
- for (const answer of [undefined, "", " "]) {
48
- prompted = answer;
49
- const cancelled = load("old-name");
50
- await cancelled.run("");
51
- assert.equal(cancelled.calls.name, undefined, `"${answer}" must not rename`);
52
- assert.equal(cancelled.calls.title, undefined, `"${answer}" must not retitle`);
53
- }
54
-
55
- console.log("umbra-rename.check.ts ok - argument form, prompt form, 3 cancel cases");
@@ -1,22 +0,0 @@
1
- import { basename } from "node:path";
2
- import type { ExtensionAPI, ExtensionCommandContext } from "@earendil-works/pi-coding-agent";
3
-
4
- // /umb-rename sets both names at once: the session display name (session selector,
5
- // /resume) and the terminal window/tab title, which are separate APIs in pi.
6
- export default function (pi: ExtensionAPI) {
7
- pi.registerCommand("umb-rename", {
8
- description: "Rename this session and the terminal title: /umb-rename <name>",
9
- handler: async (args: string, ctx: ExtensionCommandContext) => {
10
- // setSessionName lives on the API object; setTitle lives on the command context's UI.
11
- let newName = args.trim() || (await ctx.ui.input("Rename session", pi.getSessionName() ?? ""))?.trim();
12
- if (!newName) return;
13
-
14
- // The prefix belongs to the terminal title only. pi builds that title itself as
15
- // `π - <session name> - <cwd>`, so putting π in the session name too spelled it twice
16
- // in the tab and once more on the input bar, which shows the session name alone.
17
- pi.setSessionName(newName);
18
- ctx.ui.setTitle(`π - ${newName} - ${basename(ctx.sessionManager.getCwd())}`);
19
- ctx.ui.notify(`renamed: ${newName}`);
20
- },
21
- });
22
- }
@@ -1,36 +0,0 @@
1
- {
2
- "name": "pi-umbra-rename",
3
- "version": "0.1.3",
4
- "description": "One command that renames a pi session and the terminal window title together, which pi keeps behind two separate APIs.",
5
- "keywords": [
6
- "pi-package",
7
- "pi",
8
- "pi-extension",
9
- "session",
10
- "rename",
11
- "title"
12
- ],
13
- "author": "grkn",
14
- "license": "MIT",
15
- "repository": {
16
- "type": "git",
17
- "url": "git+https://github.com/grknbyk/pi-umbra.git",
18
- "directory": "pi-umbra-rename"
19
- },
20
- "homepage": "https://github.com/grknbyk/pi-umbra/tree/main/pi-umbra-rename#readme",
21
- "bugs": "https://github.com/grknbyk/pi-umbra/issues",
22
- "files": [
23
- "extensions",
24
- "checks",
25
- "README.md",
26
- "LICENSE"
27
- ],
28
- "pi": {
29
- "extensions": [
30
- "./extensions"
31
- ]
32
- },
33
- "peerDependencies": {
34
- "@earendil-works/pi-coding-agent": "*"
35
- }
36
- }