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.
- package/README.md +3 -4
- package/node_modules/pi-umbra-copy-chat/README.md +4 -7
- package/node_modules/pi-umbra-copy-chat/extensions/umbra-copy-chat.ts +16 -54
- package/node_modules/pi-umbra-copy-chat/package.json +1 -1
- package/node_modules/pi-umbra-help/checks/umbra-help.check.ts +10 -1
- package/node_modules/pi-umbra-help/extensions/umbra-help.ts +31 -0
- package/node_modules/pi-umbra-help/package.json +1 -1
- package/node_modules/pi-umbra-inputbar/README.md +1 -1
- package/node_modules/pi-umbra-inputbar/extensions/umbra-inputbar.ts +1 -1
- package/node_modules/pi-umbra-inputbar/package.json +2 -2
- package/node_modules/pi-umbra-shimmer/package.json +1 -1
- package/node_modules/pi-umbra-shimmer/patch.mjs +23 -21
- package/node_modules/pi-umbra-skill-matcher/package.json +1 -1
- package/node_modules/pi-umbra-skill-matcher/patch.mjs +7 -0
- package/node_modules/pi-umbra-subagents/README.md +19 -4
- package/node_modules/pi-umbra-subagents/extensions/umbra-subagents/bar/bar-line.ts +9 -2
- package/node_modules/pi-umbra-subagents/extensions/umbra-subagents/bar.check.ts +10 -1
- package/node_modules/pi-umbra-subagents/extensions/umbra-subagents/fan/index.ts +50 -5
- package/node_modules/pi-umbra-subagents/extensions/umbra-subagents/fan/spec.ts +24 -14
- package/node_modules/pi-umbra-subagents/extensions/umbra-subagents/fan/store.check.ts +119 -4
- package/node_modules/pi-umbra-subagents/extensions/umbra-subagents/fan/store.ts +138 -58
- package/node_modules/pi-umbra-subagents/extensions/umbra-subagents/fan/worktree.ts +156 -0
- package/node_modules/pi-umbra-subagents/extensions/umbra-subagents/skills/delegate/state.ts +19 -0
- package/node_modules/pi-umbra-subagents/extensions/umbra-subagents/skills/fan/SKILL.md +25 -4
- package/node_modules/pi-umbra-subagents/package.json +2 -2
- package/node_modules/pi-umbra-theme/checks/umbra-background.check.ts +7 -1
- package/node_modules/pi-umbra-theme/extensions/umbra-background.ts +2 -1
- package/node_modules/pi-umbra-theme/package.json +3 -3
- package/package.json +9 -12
- package/node_modules/pi-umbra-rename/LICENSE +0 -21
- package/node_modules/pi-umbra-rename/README.md +0 -18
- package/node_modules/pi-umbra-rename/checks/umbra-rename.check.ts +0 -55
- package/node_modules/pi-umbra-rename/extensions/umbra-rename.ts +0 -22
- 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
|
|
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
|
|
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.
|
|
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.
|
|
4
|
-
"description": "Parallel
|
|
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({
|
|
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
|
-
|
|
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.
|
|
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.
|
|
4
|
-
"description": "Every small thing pi does not build in, in one install: seven themes and the chrome around them, the input bar,
|
|
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.
|
|
47
|
+
"pi-umbra-theme": "0.3.0",
|
|
49
48
|
"pi-umbra-ask": "0.1.2",
|
|
50
|
-
"pi-umbra-copy-chat": "0.1.
|
|
51
|
-
"pi-umbra-help": "0.1.
|
|
52
|
-
"pi-umbra-inputbar": "0.1.
|
|
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-
|
|
55
|
-
"pi-umbra-
|
|
56
|
-
"pi-umbra-
|
|
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
|
-
}
|