pi-cc-extensions 0.1.1 → 0.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +23 -2
- package/assets/readme/welcome.webp +0 -0
- package/extensions/ccstyle-powerline/git-status.ts +89 -33
- package/extensions/ccstyle-powerline/powerline.ts +1 -1
- package/extensions/ccstyle-powerline/presets.ts +7 -7
- package/extensions/ccstyle-powerline/segments.ts +11 -1
- package/extensions/ccstyle-powerline/types.ts +3 -0
- package/extensions/claude-code-style.ts +19 -0
- package/extensions/session-reference/README.md +1 -1
- package/extensions/session-reference/index.ts +100 -68
- package/package.json +4 -2
package/README.md
CHANGED
|
@@ -18,6 +18,13 @@
|
|
|
18
18
|
## 截图
|
|
19
19
|
|
|
20
20
|
<table>
|
|
21
|
+
<tr>
|
|
22
|
+
<td colspan="2" align="center">
|
|
23
|
+
<img src="./assets/readme/welcome.webp" width="100%" alt="Pi 的 Welcome 界面,展示 Tips、已加载上下文、扩展和最近会话">
|
|
24
|
+
<br>
|
|
25
|
+
<sub><b>Welcome 界面</b><br>启动时查看已加载上下文、扩展与最近会话</sub>
|
|
26
|
+
</td>
|
|
27
|
+
</tr>
|
|
21
28
|
<tr>
|
|
22
29
|
<td width="50%" align="center">
|
|
23
30
|
<img src="./assets/readme/showcase.webp" width="100%" alt="Pi 终端中的 Claude Code 风格扩展界面,包含工具调用摘要、Diff 预览和 Powerline 状态栏">
|
|
@@ -86,7 +93,7 @@
|
|
|
86
93
|
2. 从 `[Session] ...` 或 `[SubAgent] ...` 补全项中选择要引用的上下文。
|
|
87
94
|
3. 提交时以 `@session:<session-id>` 引用其当前有效上下文。
|
|
88
95
|
|
|
89
|
-
Session 模糊查询默认显示
|
|
96
|
+
Session 模糊查询默认显示 5 个候选;若已加载 `pi-subagents`,也可直接引用现有 SubAgent。一次提示词可以引用多个 Session;扩展会自动去重,并限制注入规模以避免上下文无限膨胀。更多细节见 [`extensions/session-reference/README.md`](./extensions/session-reference/README.md)。
|
|
90
97
|
|
|
91
98
|
## 安装
|
|
92
99
|
|
|
@@ -133,9 +140,23 @@ pi install /absolute/path/to/pi-cc-extensions
|
|
|
133
140
|
- Node.js `>=22.19.0`
|
|
134
141
|
- 作为 Pi package 加载,入口由根目录 `package.json` 的 `pi.extensions` 显式声明
|
|
135
142
|
|
|
136
|
-
##
|
|
143
|
+
## 发布
|
|
144
|
+
|
|
145
|
+
发布前检查:
|
|
137
146
|
|
|
138
147
|
```bash
|
|
139
148
|
npm test
|
|
140
149
|
npm pack --dry-run
|
|
141
150
|
```
|
|
151
|
+
|
|
152
|
+
使用 npm 发布。未指定 tag 时使用 `latest`:
|
|
153
|
+
|
|
154
|
+
```bash
|
|
155
|
+
npm run release
|
|
156
|
+
npm run release -- --tag next
|
|
157
|
+
npm run release -- --tag beta
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
## 致谢
|
|
161
|
+
|
|
162
|
+
本项目中的 Powerline 状态栏、Welcome 界面、Working Vibes 等功能基于 [nicobailon/pi-powerline-footer](https://github.com/nicobailon/pi-powerline-footer) 二次开发,感谢原作者的开源贡献。
|
|
Binary file
|
|
@@ -5,6 +5,8 @@ interface CachedGitStatus {
|
|
|
5
5
|
staged: number;
|
|
6
6
|
unstaged: number;
|
|
7
7
|
untracked: number;
|
|
8
|
+
added: number;
|
|
9
|
+
deleted: number;
|
|
8
10
|
timestamp: number;
|
|
9
11
|
}
|
|
10
12
|
|
|
@@ -19,19 +21,25 @@ const CACHE_TTL_MS = 1000; // 1 second for file status
|
|
|
19
21
|
const BRANCH_TTL_MS = 500; // Shorter TTL so branch updates quickly after invalidation
|
|
20
22
|
let cachedStatus: CachedGitStatus | null = null;
|
|
21
23
|
let cachedBranch: CachedBranch | null = null;
|
|
24
|
+
let statusCwd: string | null = null;
|
|
25
|
+
let branchCwd: string | null = null;
|
|
22
26
|
let pendingFetch: Promise<void> | null = null;
|
|
23
27
|
let pendingBranchFetch: Promise<void> | null = null;
|
|
28
|
+
let fetchCwd: string | null = null;
|
|
29
|
+
let branchFetchCwd: string | null = null;
|
|
24
30
|
let invalidationCounter = 0; // Track invalidations to prevent stale updates
|
|
25
31
|
let branchInvalidationCounter = 0;
|
|
26
32
|
|
|
27
33
|
/**
|
|
28
|
-
* Parse git status --porcelain output
|
|
29
|
-
*
|
|
34
|
+
* Parse git status --porcelain output.
|
|
35
|
+
*
|
|
30
36
|
* Format: XY filename
|
|
31
37
|
* X = index status, Y = working tree status
|
|
32
38
|
* ?? = untracked
|
|
33
|
-
*
|
|
34
|
-
*
|
|
39
|
+
*
|
|
40
|
+
* Each file is counted exactly once:
|
|
41
|
+
* - Y ≠ ' ' → unstaged (working tree has changes; if X also shows staged, Y wins)
|
|
42
|
+
* - X ≠ ' ' && X ≠ '?' → staged only (index has changes, working tree is clean)
|
|
35
43
|
*/
|
|
36
44
|
function parseGitStatusOutput(output: string): { staged: number; unstaged: number; untracked: number } {
|
|
37
45
|
let staged = 0;
|
|
@@ -48,24 +56,24 @@ function parseGitStatusOutput(output: string): { staged: number; unstaged: numbe
|
|
|
48
56
|
continue;
|
|
49
57
|
}
|
|
50
58
|
|
|
51
|
-
//
|
|
52
|
-
|
|
53
|
-
staged++;
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
// Y position (working tree/unstaged)
|
|
59
|
+
// Y position (working tree) takes priority — if the file has working-tree
|
|
60
|
+
// changes at all, count it as unstaged.
|
|
57
61
|
if (y && y !== " ") {
|
|
58
62
|
unstaged++;
|
|
63
|
+
} else if (x && x !== " " && x !== "?") {
|
|
64
|
+
// X position (index) only counted when working tree is clean.
|
|
65
|
+
staged++;
|
|
59
66
|
}
|
|
60
67
|
}
|
|
61
68
|
|
|
62
69
|
return { staged, unstaged, untracked };
|
|
63
70
|
}
|
|
64
71
|
|
|
65
|
-
function runGit(args: string[], timeoutMs = 200): Promise<string | null> {
|
|
72
|
+
function runGit(args: string[], cwd: string, timeoutMs = 200): Promise<string | null> {
|
|
66
73
|
return new Promise((resolve) => {
|
|
67
74
|
const proc = spawn("git", args, {
|
|
68
75
|
stdio: ["ignore", "pipe", "pipe"],
|
|
76
|
+
cwd,
|
|
69
77
|
});
|
|
70
78
|
|
|
71
79
|
let stdout = "";
|
|
@@ -83,7 +91,7 @@ function runGit(args: string[], timeoutMs = 200): Promise<string | null> {
|
|
|
83
91
|
});
|
|
84
92
|
|
|
85
93
|
proc.on("close", (code) => {
|
|
86
|
-
finish(code === 0 ? stdout.
|
|
94
|
+
finish(code === 0 ? stdout.replace(/\n+$/, "") : null);
|
|
87
95
|
});
|
|
88
96
|
|
|
89
97
|
proc.on("error", () => {
|
|
@@ -101,31 +109,50 @@ function runGit(args: string[], timeoutMs = 200): Promise<string | null> {
|
|
|
101
109
|
* Fetch current git branch asynchronously.
|
|
102
110
|
* For detached HEAD, returns the short commit SHA (matches provider's "detached" behavior).
|
|
103
111
|
*/
|
|
104
|
-
async function fetchGitBranch(): Promise<string | null> {
|
|
105
|
-
const branch = await runGit(["branch", "--show-current"]);
|
|
112
|
+
async function fetchGitBranch(cwd: string): Promise<string | null> {
|
|
113
|
+
const branch = await runGit(["branch", "--show-current"], cwd);
|
|
106
114
|
if (branch === null) return null;
|
|
107
115
|
if (branch) return branch;
|
|
108
116
|
|
|
109
|
-
const sha = await runGit(["rev-parse", "--short", "HEAD"]);
|
|
117
|
+
const sha = await runGit(["rev-parse", "--short", "HEAD"], cwd);
|
|
110
118
|
return sha ? `${sha} (detached)` : "detached";
|
|
111
119
|
}
|
|
112
120
|
|
|
113
121
|
/**
|
|
114
|
-
*
|
|
122
|
+
* Parse git diff --shortstat HEAD output.
|
|
123
|
+
* Format: " N files changed, M insertions(+), K deletions(-)"
|
|
124
|
+
*/
|
|
125
|
+
function parseDiffStat(output: string): { added: number; deleted: number } {
|
|
126
|
+
const added = (output.match(/(\d+) insertion/) ?? [])[1];
|
|
127
|
+
const deleted = (output.match(/(\d+) deletion/) ?? [])[1];
|
|
128
|
+
return {
|
|
129
|
+
added: added ? Number(added) : 0,
|
|
130
|
+
deleted: deleted ? Number(deleted) : 0,
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Fetch line-level diff stats (additions / deletions) from HEAD.
|
|
115
136
|
*/
|
|
116
|
-
async function
|
|
117
|
-
const output = await runGit(["
|
|
137
|
+
async function fetchDiffStat(cwd: string): Promise<{ added: number; deleted: number } | null> {
|
|
138
|
+
const output = await runGit(["diff", "--shortstat", "HEAD"], cwd, 1000);
|
|
118
139
|
if (output === null) return null;
|
|
119
|
-
return
|
|
140
|
+
return parseDiffStat(output);
|
|
120
141
|
}
|
|
121
142
|
|
|
122
143
|
/**
|
|
123
144
|
* Get the current git branch with caching.
|
|
124
145
|
* Falls back to provider branch if our cache is empty.
|
|
125
146
|
*/
|
|
126
|
-
export function getCurrentBranch(providerBranch: string | null): string | null {
|
|
147
|
+
export function getCurrentBranch(providerBranch: string | null, cwd: string): string | null {
|
|
127
148
|
const now = Date.now();
|
|
128
149
|
|
|
150
|
+
// Invalidate cache if cwd changed (different repo)
|
|
151
|
+
if (branchCwd !== cwd) {
|
|
152
|
+
cachedBranch = null;
|
|
153
|
+
branchCwd = cwd;
|
|
154
|
+
}
|
|
155
|
+
|
|
129
156
|
// Return cached if fresh
|
|
130
157
|
if (cachedBranch && now - cachedBranch.timestamp < BRANCH_TTL_MS) {
|
|
131
158
|
return cachedBranch.branch;
|
|
@@ -134,9 +161,10 @@ export function getCurrentBranch(providerBranch: string | null): string | null {
|
|
|
134
161
|
// Trigger background fetch if not already pending
|
|
135
162
|
if (!pendingBranchFetch) {
|
|
136
163
|
const fetchId = branchInvalidationCounter;
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
if
|
|
164
|
+
branchFetchCwd = cwd;
|
|
165
|
+
pendingBranchFetch = fetchGitBranch(cwd).then((result) => {
|
|
166
|
+
// Cache result if no invalidation happened AND cwd hasn't changed
|
|
167
|
+
if (fetchId === branchInvalidationCounter && branchFetchCwd === cwd) {
|
|
140
168
|
cachedBranch = {
|
|
141
169
|
branch: result,
|
|
142
170
|
timestamp: Date.now(),
|
|
@@ -156,12 +184,18 @@ export function getCurrentBranch(providerBranch: string | null): string | null {
|
|
|
156
184
|
* This is designed for synchronous render() calls - returns last known value
|
|
157
185
|
* while refreshing in background.
|
|
158
186
|
*/
|
|
159
|
-
export function getGitStatus(providerBranch: string | null, pollingMode: GitPollingMode = "full"): GitStatus {
|
|
187
|
+
export function getGitStatus(providerBranch: string | null, cwd: string, pollingMode: GitPollingMode = "full"): GitStatus {
|
|
160
188
|
const now = Date.now();
|
|
161
|
-
const branch = pollingMode === "off" ? providerBranch : getCurrentBranch(providerBranch);
|
|
189
|
+
const branch = pollingMode === "off" ? providerBranch : getCurrentBranch(providerBranch, cwd);
|
|
162
190
|
|
|
163
191
|
if (pollingMode !== "full") {
|
|
164
|
-
return { branch, staged: 0, unstaged: 0, untracked: 0 };
|
|
192
|
+
return { branch, staged: 0, unstaged: 0, untracked: 0, added: 0, deleted: 0 };
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// Invalidate cache if cwd changed (different repo)
|
|
196
|
+
if (statusCwd !== cwd) {
|
|
197
|
+
cachedStatus = null;
|
|
198
|
+
statusCwd = cwd;
|
|
165
199
|
}
|
|
166
200
|
|
|
167
201
|
// Return cached if fresh
|
|
@@ -171,18 +205,38 @@ export function getGitStatus(providerBranch: string | null, pollingMode: GitPoll
|
|
|
171
205
|
staged: cachedStatus.staged,
|
|
172
206
|
unstaged: cachedStatus.unstaged,
|
|
173
207
|
untracked: cachedStatus.untracked,
|
|
208
|
+
added: cachedStatus.added,
|
|
209
|
+
deleted: cachedStatus.deleted,
|
|
174
210
|
};
|
|
175
211
|
}
|
|
176
212
|
|
|
177
213
|
// Trigger background fetch if not already pending
|
|
178
214
|
if (!pendingFetch) {
|
|
179
215
|
const fetchId = invalidationCounter; // Capture current counter
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
216
|
+
fetchCwd = cwd;
|
|
217
|
+
pendingFetch = Promise.all([
|
|
218
|
+
fetchGitStatus(cwd),
|
|
219
|
+
fetchDiffStat(cwd),
|
|
220
|
+
]).then(([statusResult, diffResult]) => {
|
|
221
|
+
// Cache result if no invalidation happened AND cwd hasn't changed
|
|
222
|
+
if (fetchId === invalidationCounter && fetchCwd === cwd) {
|
|
223
|
+
cachedStatus = statusResult
|
|
224
|
+
? {
|
|
225
|
+
staged: statusResult.staged,
|
|
226
|
+
unstaged: statusResult.unstaged,
|
|
227
|
+
untracked: statusResult.untracked,
|
|
228
|
+
added: diffResult?.added ?? 0,
|
|
229
|
+
deleted: diffResult?.deleted ?? 0,
|
|
230
|
+
timestamp: Date.now(),
|
|
231
|
+
}
|
|
232
|
+
: {
|
|
233
|
+
staged: 0,
|
|
234
|
+
unstaged: 0,
|
|
235
|
+
untracked: 0,
|
|
236
|
+
added: 0,
|
|
237
|
+
deleted: 0,
|
|
238
|
+
timestamp: Date.now(),
|
|
239
|
+
};
|
|
186
240
|
}
|
|
187
241
|
pendingFetch = null;
|
|
188
242
|
});
|
|
@@ -195,10 +249,12 @@ export function getGitStatus(providerBranch: string | null, pollingMode: GitPoll
|
|
|
195
249
|
staged: cachedStatus.staged,
|
|
196
250
|
unstaged: cachedStatus.unstaged,
|
|
197
251
|
untracked: cachedStatus.untracked,
|
|
252
|
+
added: cachedStatus.added,
|
|
253
|
+
deleted: cachedStatus.deleted,
|
|
198
254
|
};
|
|
199
255
|
}
|
|
200
256
|
|
|
201
|
-
return { branch, staged: 0, unstaged: 0, untracked: 0 };
|
|
257
|
+
return { branch, staged: 0, unstaged: 0, untracked: 0, added: 0, deleted: 0 };
|
|
202
258
|
}
|
|
203
259
|
|
|
204
260
|
/**
|
|
@@ -2181,7 +2181,7 @@ export default function powerlineFooter(pi: ExtensionAPI): PowerlineController {
|
|
|
2181
2181
|
|
|
2182
2182
|
// Get git status (cached)
|
|
2183
2183
|
const gitBranch = footerDataRef?.getGitBranch() ?? null;
|
|
2184
|
-
const gitStatus = getGitStatus(gitBranch, segmentOptions.git?.polling);
|
|
2184
|
+
const gitStatus = getGitStatus(gitBranch, ctx.cwd, segmentOptions.git?.polling);
|
|
2185
2185
|
const extensionStatuses = footerDataRef?.getExtensionStatuses() ?? new Map();
|
|
2186
2186
|
const customItemsById = new Map(config.customItems.map((item) => [item.id, item]));
|
|
2187
2187
|
const hiddenExtensionStatusKeys = collectHiddenExtensionStatusKeys(config.customItems);
|
|
@@ -31,7 +31,7 @@ export const PRESETS: Record<StatusLinePreset, PresetDef> = {
|
|
|
31
31
|
segmentOptions: {
|
|
32
32
|
model: { showThinkingLevel: false },
|
|
33
33
|
path: { mode: "basename" },
|
|
34
|
-
git: { showBranch: true, showStaged: true, showUnstaged: true, showUntracked: true },
|
|
34
|
+
git: { showBranch: true, showStaged: true, showUnstaged: true, showUntracked: true, showLines: true },
|
|
35
35
|
},
|
|
36
36
|
},
|
|
37
37
|
|
|
@@ -42,7 +42,7 @@ export const PRESETS: Record<StatusLinePreset, PresetDef> = {
|
|
|
42
42
|
colors: MINIMAL_COLORS,
|
|
43
43
|
segmentOptions: {
|
|
44
44
|
path: { mode: "basename" },
|
|
45
|
-
git: { showBranch: true, showStaged: false, showUnstaged: false, showUntracked: false },
|
|
45
|
+
git: { showBranch: true, showStaged: false, showUnstaged: false, showUntracked: false, showLines: false },
|
|
46
46
|
},
|
|
47
47
|
},
|
|
48
48
|
|
|
@@ -53,7 +53,7 @@ export const PRESETS: Record<StatusLinePreset, PresetDef> = {
|
|
|
53
53
|
colors: DEFAULT_COLORS,
|
|
54
54
|
segmentOptions: {
|
|
55
55
|
model: { showThinkingLevel: true },
|
|
56
|
-
git: { showBranch: true, showStaged: true, showUnstaged: true, showUntracked: false },
|
|
56
|
+
git: { showBranch: true, showStaged: true, showUnstaged: true, showUntracked: false, showLines: true },
|
|
57
57
|
},
|
|
58
58
|
},
|
|
59
59
|
|
|
@@ -65,7 +65,7 @@ export const PRESETS: Record<StatusLinePreset, PresetDef> = {
|
|
|
65
65
|
segmentOptions: {
|
|
66
66
|
model: { showThinkingLevel: false },
|
|
67
67
|
path: { mode: "abbreviated", maxLength: 50 },
|
|
68
|
-
git: { showBranch: true, showStaged: true, showUnstaged: true, showUntracked: true },
|
|
68
|
+
git: { showBranch: true, showStaged: true, showUnstaged: true, showUntracked: true, showLines: true },
|
|
69
69
|
time: { format: "24h", showSeconds: false },
|
|
70
70
|
},
|
|
71
71
|
},
|
|
@@ -78,7 +78,7 @@ export const PRESETS: Record<StatusLinePreset, PresetDef> = {
|
|
|
78
78
|
segmentOptions: {
|
|
79
79
|
model: { showThinkingLevel: false },
|
|
80
80
|
path: { mode: "abbreviated", maxLength: 60 },
|
|
81
|
-
git: { showBranch: true, showStaged: true, showUnstaged: true, showUntracked: true },
|
|
81
|
+
git: { showBranch: true, showStaged: true, showUnstaged: true, showUntracked: true, showLines: true },
|
|
82
82
|
time: { format: "24h", showSeconds: true },
|
|
83
83
|
},
|
|
84
84
|
},
|
|
@@ -91,7 +91,7 @@ export const PRESETS: Record<StatusLinePreset, PresetDef> = {
|
|
|
91
91
|
segmentOptions: {
|
|
92
92
|
model: { showThinkingLevel: true },
|
|
93
93
|
path: { mode: "abbreviated", maxLength: 40 },
|
|
94
|
-
git: { showBranch: true, showStaged: true, showUnstaged: true, showUntracked: true },
|
|
94
|
+
git: { showBranch: true, showStaged: true, showUnstaged: true, showUntracked: true, showLines: false },
|
|
95
95
|
},
|
|
96
96
|
},
|
|
97
97
|
|
|
@@ -100,7 +100,7 @@ export const PRESETS: Record<StatusLinePreset, PresetDef> = {
|
|
|
100
100
|
rightSegments: ["token_total", "cost", "context_pct"],
|
|
101
101
|
separator: "pipe",
|
|
102
102
|
colors: DEFAULT_COLORS,
|
|
103
|
-
segmentOptions: {
|
|
103
|
+
segmentOptions: {
|
|
104
104
|
},
|
|
105
105
|
};
|
|
106
106
|
|
|
@@ -130,7 +130,7 @@ const gitSegment: StatusLineSegment = {
|
|
|
130
130
|
render(ctx) {
|
|
131
131
|
const icons = getIcons();
|
|
132
132
|
const opts = ctx.options.git ?? {};
|
|
133
|
-
const { branch, staged, unstaged, untracked } = ctx.git;
|
|
133
|
+
const { branch, staged, unstaged, untracked, added, deleted } = ctx.git;
|
|
134
134
|
const gitStatus = (staged > 0 || unstaged > 0 || untracked > 0)
|
|
135
135
|
? { staged, unstaged, untracked }
|
|
136
136
|
: null;
|
|
@@ -169,6 +169,16 @@ const gitSegment: StatusLineSegment = {
|
|
|
169
169
|
content += content ? ` ${indicatorText}` : indicatorText;
|
|
170
170
|
}
|
|
171
171
|
}
|
|
172
|
+
|
|
173
|
+
// Line-change indicators (added / deleted)
|
|
174
|
+
if (opts.showLines && (added > 0 || deleted > 0)) {
|
|
175
|
+
const lineParts: string[] = [];
|
|
176
|
+
if (added > 0) lineParts.push(applyColor(ctx.theme, "success", `+${added}`));
|
|
177
|
+
if (deleted > 0) lineParts.push(applyColor(ctx.theme, "error", `-${deleted}`));
|
|
178
|
+
if (lineParts.length > 0) {
|
|
179
|
+
content += ` ${lineParts.join(" ")}`;
|
|
180
|
+
}
|
|
181
|
+
}
|
|
172
182
|
}
|
|
173
183
|
|
|
174
184
|
if (!content) return { content: "", visible: false };
|
|
@@ -90,6 +90,7 @@ export interface StatusLineSegmentOptions {
|
|
|
90
90
|
showStaged?: boolean;
|
|
91
91
|
showUnstaged?: boolean;
|
|
92
92
|
showUntracked?: boolean;
|
|
93
|
+
showLines?: boolean;
|
|
93
94
|
polling?: "full" | "branch" | "off";
|
|
94
95
|
};
|
|
95
96
|
time?: { format?: "12h" | "24h"; showSeconds?: boolean };
|
|
@@ -136,6 +137,8 @@ export interface GitStatus {
|
|
|
136
137
|
staged: number;
|
|
137
138
|
unstaged: number;
|
|
138
139
|
untracked: number;
|
|
140
|
+
added: number;
|
|
141
|
+
deleted: number;
|
|
139
142
|
}
|
|
140
143
|
|
|
141
144
|
// Usage statistics
|
|
@@ -837,6 +837,25 @@ export default function (pi: ExtensionAPI) {
|
|
|
837
837
|
|
|
838
838
|
pi.registerCommand("ccstyle", {
|
|
839
839
|
description: "Configure Claude Code style and Powerline preset",
|
|
840
|
+
getArgumentCompletions: (prefix) => {
|
|
841
|
+
const topLevel = [
|
|
842
|
+
{ value: "on", label: "on", description: "Enable Claude Code style" },
|
|
843
|
+
{ value: "off", label: "off", description: "Disable Claude Code style" },
|
|
844
|
+
{ value: "status", label: "status", description: "Show current toggle and mode" },
|
|
845
|
+
{ value: "minimal", label: "minimal", description: "Hide most tool output in collapsed view" },
|
|
846
|
+
{ value: "compact", label: "compact", description: "Show short summaries in collapsed view" },
|
|
847
|
+
{ value: "powerline", label: "powerline", description: "Set Powerline preset (e.g. powerline compact)" },
|
|
848
|
+
];
|
|
849
|
+
// If the user typed "powerline " — suggest available presets
|
|
850
|
+
const match = prefix.match(/^powerline\s+/i);
|
|
851
|
+
if (match) {
|
|
852
|
+
const after = prefix.slice(match[0].length);
|
|
853
|
+
return powerline.getPresets()
|
|
854
|
+
.filter((p) => p.startsWith(after))
|
|
855
|
+
.map((p) => ({ value: `powerline ${p}`, label: p, description: `Powerline preset: ${p}` }));
|
|
856
|
+
}
|
|
857
|
+
return topLevel.filter((item) => item.value.startsWith(prefix));
|
|
858
|
+
},
|
|
840
859
|
handler: async (args, ctx) => {
|
|
841
860
|
const arg = args.trim().toLowerCase();
|
|
842
861
|
const parts = arg.split(/\s+/).filter(Boolean);
|
|
@@ -22,12 +22,25 @@ import {
|
|
|
22
22
|
type ReferenceSource,
|
|
23
23
|
} from "./core.ts";
|
|
24
24
|
|
|
25
|
-
const MAX_SESSION_SUGGESTIONS =
|
|
25
|
+
const MAX_SESSION_SUGGESTIONS = 5;
|
|
26
26
|
const MAX_FILE_SUGGESTIONS = 10;
|
|
27
27
|
const MAX_REFERENCED_SESSIONS = 5;
|
|
28
28
|
const MENTION_PATTERN = /(?:^|[\t ])@([^\s@]*)$/;
|
|
29
29
|
const SUBAGENT_MANAGER_KEY = Symbol.for("pi-subagents:manager");
|
|
30
30
|
|
|
31
|
+
// ── In-process subagent record tracker ──────────────────────────────
|
|
32
|
+
// pi-subagents does not expose a global manager; we track records
|
|
33
|
+
// ourselves by listening to the events it emits.
|
|
34
|
+
|
|
35
|
+
type SubagentLiveRecord = {
|
|
36
|
+
runId: string;
|
|
37
|
+
sessionId: string;
|
|
38
|
+
agent: string;
|
|
39
|
+
cwd: string;
|
|
40
|
+
startedAt: number;
|
|
41
|
+
completedAt?: number;
|
|
42
|
+
};
|
|
43
|
+
|
|
31
44
|
type ReferenceDetails = {
|
|
32
45
|
sessions: Array<{ id: string; title: string; cwd: string }>;
|
|
33
46
|
};
|
|
@@ -40,27 +53,72 @@ type SessionReference = {
|
|
|
40
53
|
messages?: unknown[];
|
|
41
54
|
};
|
|
42
55
|
|
|
43
|
-
type LiveSubagentSession = {
|
|
44
|
-
sessionManager: {
|
|
45
|
-
getSessionId(): string;
|
|
46
|
-
getCwd(): string;
|
|
47
|
-
buildSessionContext(): { messages: unknown[] };
|
|
48
|
-
};
|
|
49
|
-
sessionName?: string;
|
|
50
|
-
};
|
|
51
|
-
|
|
52
56
|
type SubagentRecord = {
|
|
53
57
|
id: string;
|
|
54
58
|
description?: string;
|
|
55
59
|
startedAt?: number;
|
|
56
60
|
completedAt?: number;
|
|
57
|
-
session?: LiveSubagentSession;
|
|
58
61
|
};
|
|
59
62
|
|
|
60
63
|
type SubagentManager = {
|
|
61
64
|
getRecord(id: string): SubagentRecord | undefined;
|
|
62
65
|
};
|
|
63
66
|
|
|
67
|
+
// Local subagent record tracking (pi-subagents does not expose a global manager).
|
|
68
|
+
const liveSubagentRecords = new Map<string, SubagentLiveRecord>();
|
|
69
|
+
|
|
70
|
+
function trackSubagentFromEvent(data: unknown): void {
|
|
71
|
+
if (!data || typeof data !== "object") return;
|
|
72
|
+
const event = data as Record<string, unknown>;
|
|
73
|
+
// subagent:async-started payload has "id" as the run ID
|
|
74
|
+
// subagent:async-complete payload has "runId" as the run ID
|
|
75
|
+
const runId = (typeof event.id === "string" ? event.id : undefined)
|
|
76
|
+
?? (typeof event.runId === "string" ? event.runId : undefined);
|
|
77
|
+
if (!runId) return;
|
|
78
|
+
const sessionId = typeof event.sessionId === "string" ? event.sessionId : undefined;
|
|
79
|
+
if (!sessionId) return;
|
|
80
|
+
|
|
81
|
+
const existing = liveSubagentRecords.get(runId);
|
|
82
|
+
if (existing) {
|
|
83
|
+
// Completion event — mark completedAt
|
|
84
|
+
if (typeof event.endedAt === "number" || typeof event.lastUpdate === "number") {
|
|
85
|
+
existing.completedAt = (typeof event.endedAt === "number" ? event.endedAt : event.lastUpdate) as number;
|
|
86
|
+
}
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
// Started event — create new record
|
|
90
|
+
const agent = typeof event.agent === "string" ? event.agent : "";
|
|
91
|
+
const cwd = typeof event.cwd === "string" ? event.cwd : "";
|
|
92
|
+
const startedAt = typeof event.startedAt === "number" ? event.startedAt as number : Date.now();
|
|
93
|
+
liveSubagentRecords.set(runId, {
|
|
94
|
+
runId,
|
|
95
|
+
sessionId,
|
|
96
|
+
agent,
|
|
97
|
+
cwd,
|
|
98
|
+
startedAt,
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function getSubagentManager(): SubagentManager | undefined {
|
|
103
|
+
// Try the global manager first (future-proof), fall back to local records.
|
|
104
|
+
const manager = (globalThis as any)[SUBAGENT_MANAGER_KEY] as SubagentManager | undefined;
|
|
105
|
+
if (manager && typeof manager.getRecord === "function") return manager;
|
|
106
|
+
// If no global manager, use our local tracking.
|
|
107
|
+
if (liveSubagentRecords.size === 0) return undefined;
|
|
108
|
+
return {
|
|
109
|
+
getRecord(id: string): SubagentRecord | undefined {
|
|
110
|
+
const record = liveSubagentRecords.get(id);
|
|
111
|
+
if (!record) return undefined;
|
|
112
|
+
return {
|
|
113
|
+
id: record.runId,
|
|
114
|
+
description: record.agent || undefined,
|
|
115
|
+
startedAt: record.startedAt,
|
|
116
|
+
completedAt: record.completedAt,
|
|
117
|
+
};
|
|
118
|
+
},
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
|
|
64
122
|
function extractMentionQuery(textBeforeCursor: string): string | undefined {
|
|
65
123
|
return textBeforeCursor.match(MENTION_PATTERN)?.[1];
|
|
66
124
|
}
|
|
@@ -108,67 +166,34 @@ function filterSessions(references: SessionReference[], query: string, currentCw
|
|
|
108
166
|
return matches.slice(0, MAX_SESSION_SUGGESTIONS).map((reference) => sessionItem(reference, currentCwd));
|
|
109
167
|
}
|
|
110
168
|
|
|
111
|
-
function
|
|
112
|
-
const manager = (globalThis as any)[SUBAGENT_MANAGER_KEY] as SubagentManager | undefined;
|
|
113
|
-
return manager && typeof manager.getRecord === "function" ? manager : undefined;
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
function textFromMessage(message: unknown): string {
|
|
117
|
-
if (!message || typeof message !== "object") return "";
|
|
118
|
-
const value = message as Record<string, unknown>;
|
|
119
|
-
if (typeof value.content === "string") return value.content;
|
|
120
|
-
if (!Array.isArray(value.content)) return "";
|
|
121
|
-
return value.content
|
|
122
|
-
.filter((part): part is Record<string, unknown> => Boolean(part) && typeof part === "object")
|
|
123
|
-
.filter((part) => part.type === "text" && typeof part.text === "string")
|
|
124
|
-
.map((part) => part.text as string)
|
|
125
|
-
.join("\n");
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
function firstUserMessage(messages: unknown[]): string {
|
|
129
|
-
for (const message of messages) {
|
|
130
|
-
if (!message || typeof message !== "object") continue;
|
|
131
|
-
const role = (message as Record<string, unknown>).role;
|
|
132
|
-
if (role !== "user") continue;
|
|
133
|
-
const text = textFromMessage(message).trim();
|
|
134
|
-
if (text) return text;
|
|
135
|
-
}
|
|
136
|
-
return "(no messages)";
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
function liveSubagentReferences(agentIds: Set<string>, currentSessionId: string): SessionReference[] {
|
|
169
|
+
function filterSessions(agentIds: Set<string>, currentSessionId: string): SessionReference[] {
|
|
140
170
|
const manager = getSubagentManager();
|
|
141
171
|
if (!manager) return [];
|
|
142
172
|
|
|
143
173
|
const references: SessionReference[] = [];
|
|
144
174
|
for (const agentId of agentIds) {
|
|
145
175
|
const record = manager.getRecord(agentId);
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
messages,
|
|
168
|
-
});
|
|
169
|
-
} catch {
|
|
170
|
-
// A subagent can finish or be disposed while the suggestion list is loading.
|
|
171
|
-
}
|
|
176
|
+
if (!record) continue;
|
|
177
|
+
|
|
178
|
+
// Try the global manager's live session first (future-proof).
|
|
179
|
+
const liveRecord = liveSubagentRecords.get(agentId);
|
|
180
|
+
const sessionId = liveRecord?.sessionId ?? record.id;
|
|
181
|
+
if (!sessionId || sessionId === currentSessionId) continue;
|
|
182
|
+
|
|
183
|
+
const name = record.description?.trim() || liveRecord?.agent || undefined;
|
|
184
|
+
const modifiedAt = record.completedAt ?? record.startedAt ?? Date.now();
|
|
185
|
+
references.push({
|
|
186
|
+
kind: "subagent",
|
|
187
|
+
referenceIds: [sessionId, agentId],
|
|
188
|
+
info: {
|
|
189
|
+
id: sessionId,
|
|
190
|
+
name,
|
|
191
|
+
cwd: liveRecord?.cwd ?? "",
|
|
192
|
+
firstMessage: "",
|
|
193
|
+
messageCount: 0,
|
|
194
|
+
modified: new Date(modifiedAt),
|
|
195
|
+
},
|
|
196
|
+
});
|
|
172
197
|
}
|
|
173
198
|
return references;
|
|
174
199
|
}
|
|
@@ -239,11 +264,18 @@ function samePath(left: string | undefined, right: string): boolean {
|
|
|
239
264
|
export default function sessionReferenceExtension(pi: ExtensionAPI): void {
|
|
240
265
|
let getAvailableReferences: (() => Promise<SessionReference[]>) | undefined;
|
|
241
266
|
const subagentIds = new Set<string>();
|
|
242
|
-
|
|
267
|
+
// pi-subagents emits "subagent:async-started" and "subagent:async-complete" events.
|
|
268
|
+
// We track records locally so @[SubAgent] suggestions work even without a global manager.
|
|
269
|
+
const subagentEventNames = ["subagent:async-started", "subagent:async-complete"];
|
|
270
|
+
const unsubscribeSubagentEvents = subagentEventNames.map(
|
|
243
271
|
(eventName) =>
|
|
244
272
|
pi.events.on(eventName, (data) => {
|
|
273
|
+
trackSubagentFromEvent(data);
|
|
245
274
|
if (!data || typeof data !== "object") return;
|
|
246
|
-
const
|
|
275
|
+
const payload = data as Record<string, unknown>;
|
|
276
|
+
// subagent:async-started uses "id", subagent:async-complete uses "runId"
|
|
277
|
+
const id = (typeof payload.id === "string" ? payload.id : undefined)
|
|
278
|
+
?? (typeof payload.runId === "string" ? payload.runId : undefined);
|
|
247
279
|
if (typeof id === "string" && id) subagentIds.add(id);
|
|
248
280
|
}),
|
|
249
281
|
);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-cc-extensions",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.2",
|
|
4
4
|
"description": "Personal extensions for the Pi coding agent",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"keywords": [
|
|
@@ -20,7 +20,9 @@
|
|
|
20
20
|
]
|
|
21
21
|
},
|
|
22
22
|
"scripts": {
|
|
23
|
-
"test": "node --test extensions/session-reference/core.test.ts"
|
|
23
|
+
"test": "node --test extensions/session-reference/core.test.ts",
|
|
24
|
+
"prepublishOnly": "npm test",
|
|
25
|
+
"release": "npm publish"
|
|
24
26
|
},
|
|
25
27
|
"peerDependencies": {
|
|
26
28
|
"@earendil-works/pi-ai": "*",
|