pi-cc-extensions 1.0.4 → 1.0.6
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.en.md +6 -6
- package/README.md +6 -6
- package/extensions/config/config.ts +16 -27
- package/extensions/config/panel.ts +49 -48
- package/extensions/feature/compact-thinking.ts +2 -3
- package/extensions/feature/context.ts +70 -2
- package/extensions/feature/reference/index.ts +2 -3
- package/extensions/feature/shell/footer-layout.ts +108 -23
- package/extensions/feature/shell/footer.ts +59 -43
- package/extensions/feature/shell/prompt-editor.ts +96 -0
- package/extensions/index.ts +6 -0
- package/extensions/renderer/compact-mode.ts +47 -40
- package/extensions/renderer/default-mode.ts +7 -4
- package/extensions/renderer/mouse/interaction.ts +18 -4
- package/extensions/renderer/mouse/scroll.ts +23 -8
- package/extensions/renderer/mouse/sticky-prompt.ts +2 -2
- package/extensions/renderer/tool/ccstyle-tool.ts +24 -12
- package/extensions/renderer/tool/codemode.ts +121 -6
- package/extensions/renderer/tool/diff/diff-limits.ts +3 -2
- package/extensions/renderer/tool/diff/diff-palette.ts +39 -8
- package/extensions/renderer/tool/diff/diff-presentation.ts +2 -2
- package/extensions/renderer/tool/grouping.ts +13 -13
- package/extensions/renderer/tool/message-display.ts +3 -2
- package/extensions/renderer/tool/names.ts +49 -49
- package/extensions/renderer/tool/result.ts +24 -0
- package/extensions/renderer/tool/tool-state.ts +51 -2
- package/package.json +1 -1
- package/themes/cc-dark.json +72 -71
- package/themes/cc-light.json +72 -71
|
@@ -6,7 +6,48 @@
|
|
|
6
6
|
export const PI_USAGE_KEY = "pi-usage";
|
|
7
7
|
export const PI_USAGE_SOURCE = "@narumitw/pi-usage";
|
|
8
8
|
const LEGACY_PI_USAGE_KEYS = new Set(["localUsage", "piUsage"]);
|
|
9
|
-
|
|
9
|
+
|
|
10
|
+
/** 内置芯片。不是 setStatus key,由本扩展自己画。 */
|
|
11
|
+
export const FOOTER_MODEL_KEY = "model";
|
|
12
|
+
export const FOOTER_CONTEXT_KEY = "context";
|
|
13
|
+
export const FOOTER_CACHE_KEY = "cache";
|
|
14
|
+
export const FOOTER_RATE_KEY = "rate";
|
|
15
|
+
export const FOOTER_COST_KEY = "cost";
|
|
16
|
+
export const FOOTER_CWD_KEY = "cwd";
|
|
17
|
+
export const FOOTER_GIT_KEY = "git";
|
|
18
|
+
|
|
19
|
+
/** 默认第一行,顺序与改成可排布之前的硬编码一致。 */
|
|
20
|
+
export const DEFAULT_FOOTER_LINE1_KEYS = [
|
|
21
|
+
FOOTER_MODEL_KEY,
|
|
22
|
+
FOOTER_CONTEXT_KEY,
|
|
23
|
+
FOOTER_CACHE_KEY,
|
|
24
|
+
FOOTER_RATE_KEY,
|
|
25
|
+
FOOTER_COST_KEY,
|
|
26
|
+
PI_USAGE_KEY,
|
|
27
|
+
] as const;
|
|
28
|
+
|
|
29
|
+
/** 默认第二行。cwd 在前,git 在后。 */
|
|
30
|
+
export const DEFAULT_FOOTER_LINE2_KEYS = [FOOTER_CWD_KEY, FOOTER_GIT_KEY] as const;
|
|
31
|
+
|
|
32
|
+
/** 改成可排布之前就写死的项。pi-usage 不算:旧配置里本来就有它。 */
|
|
33
|
+
const LEGACY_FIXED_FOOTER_KEYS = [
|
|
34
|
+
FOOTER_MODEL_KEY,
|
|
35
|
+
FOOTER_CONTEXT_KEY,
|
|
36
|
+
FOOTER_CACHE_KEY,
|
|
37
|
+
FOOTER_RATE_KEY,
|
|
38
|
+
FOOTER_COST_KEY,
|
|
39
|
+
FOOTER_CWD_KEY,
|
|
40
|
+
FOOTER_GIT_KEY,
|
|
41
|
+
] as const;
|
|
42
|
+
|
|
43
|
+
const BUILTIN_FOOTER_KEYS = new Set<string>([
|
|
44
|
+
...DEFAULT_FOOTER_LINE1_KEYS,
|
|
45
|
+
...DEFAULT_FOOTER_LINE2_KEYS,
|
|
46
|
+
]);
|
|
47
|
+
|
|
48
|
+
export function isBuiltinFooterKey(key: string): boolean {
|
|
49
|
+
return BUILTIN_FOOTER_KEYS.has(key);
|
|
50
|
+
}
|
|
10
51
|
|
|
11
52
|
export type FooterLineId = 1 | 2 | 3;
|
|
12
53
|
|
|
@@ -19,18 +60,20 @@ export type FooterChipLayout = {
|
|
|
19
60
|
|
|
20
61
|
export const DEFAULT_FOOTER_CHIP_LAYOUT: FooterChipLayout = {
|
|
21
62
|
footerHiddenKeys: [],
|
|
22
|
-
footerLine1Keys: [
|
|
23
|
-
footerLine2Keys: [],
|
|
63
|
+
footerLine1Keys: [...DEFAULT_FOOTER_LINE1_KEYS],
|
|
64
|
+
footerLine2Keys: [...DEFAULT_FOOTER_LINE2_KEYS],
|
|
24
65
|
footerLine3Keys: [],
|
|
25
66
|
};
|
|
26
67
|
|
|
68
|
+
/** 别的扩展写的 model 状态与本扩展的模型芯片重名,不当插件芯片接纳。配置里的 model 不受影响。 */
|
|
27
69
|
export function isSkippedFooterStatusKey(key: string): boolean {
|
|
28
|
-
return key ===
|
|
70
|
+
return key === FOOTER_MODEL_KEY;
|
|
29
71
|
}
|
|
30
72
|
|
|
31
|
-
/**
|
|
73
|
+
/** 未单独配置时的默认行:内置项按原来的行,usage 类在 line1,其余 line2。不会默认进 line3。 */
|
|
32
74
|
export function defaultFooterLine(key: string): FooterLineId {
|
|
33
|
-
if (
|
|
75
|
+
if ((DEFAULT_FOOTER_LINE1_KEYS as readonly string[]).includes(key)) return 1;
|
|
76
|
+
if ((DEFAULT_FOOTER_LINE2_KEYS as readonly string[]).includes(key)) return 2;
|
|
34
77
|
if (/usage|quota|balance/i.test(key)) return 1;
|
|
35
78
|
return 2;
|
|
36
79
|
}
|
|
@@ -43,7 +86,7 @@ export function normalizeFooterKeyList(value: unknown): string[] {
|
|
|
43
86
|
if (typeof item !== "string") continue;
|
|
44
87
|
let key = item.trim();
|
|
45
88
|
if (LEGACY_PI_USAGE_KEYS.has(key)) key = PI_USAGE_KEY;
|
|
46
|
-
if (!key ||
|
|
89
|
+
if (!key || seen.has(key)) continue;
|
|
47
90
|
seen.add(key);
|
|
48
91
|
out.push(key);
|
|
49
92
|
}
|
|
@@ -60,37 +103,81 @@ function takeUnique(keys: string[], seen: Set<string>): string[] {
|
|
|
60
103
|
return out;
|
|
61
104
|
}
|
|
62
105
|
|
|
63
|
-
/**
|
|
106
|
+
/**
|
|
107
|
+
* 旧配置只排插件芯片,内置项是硬编码的。
|
|
108
|
+
* 三条行都没出现过内置 key 时,按原位置补上,已有插件顺序不动。
|
|
109
|
+
*/
|
|
110
|
+
function ensureBuiltinKeys(
|
|
111
|
+
line1: string[],
|
|
112
|
+
line2: string[],
|
|
113
|
+
line3: string[],
|
|
114
|
+
): { footerLine1Keys: string[]; footerLine2Keys: string[]; footerLine3Keys: string[] } {
|
|
115
|
+
const seen = new Set([...line1, ...line2, ...line3]);
|
|
116
|
+
if (LEGACY_FIXED_FOOTER_KEYS.some((key) => seen.has(key))) {
|
|
117
|
+
return { footerLine1Keys: line1, footerLine2Keys: line2, footerLine3Keys: line3 };
|
|
118
|
+
}
|
|
119
|
+
const line1Builtins = DEFAULT_FOOTER_LINE1_KEYS.filter((key) => key !== PI_USAGE_KEY);
|
|
120
|
+
const usageAt = line1.indexOf(PI_USAGE_KEY);
|
|
121
|
+
const footerLine1Keys =
|
|
122
|
+
usageAt >= 0
|
|
123
|
+
? [...line1.slice(0, usageAt), ...line1Builtins, ...line1.slice(usageAt)]
|
|
124
|
+
: [...line1Builtins, ...line1];
|
|
125
|
+
return {
|
|
126
|
+
footerLine1Keys,
|
|
127
|
+
footerLine2Keys: [...DEFAULT_FOOTER_LINE2_KEYS, ...line2],
|
|
128
|
+
footerLine3Keys: line3,
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/** 跨行去重(line1 → line2 → line3);旧配置补上内置芯片。 */
|
|
64
133
|
export function normalizeFooterChipLayout(source: Record<string, unknown>): FooterChipLayout {
|
|
65
134
|
const hasHidden = Array.isArray(source.footerHiddenKeys);
|
|
66
135
|
const hasAnyLine = [source.footerLine1Keys, source.footerLine2Keys, source.footerLine3Keys].some(
|
|
67
136
|
Array.isArray,
|
|
68
137
|
);
|
|
69
|
-
const seen = new Set<string>();
|
|
70
|
-
let footerLine1Keys = takeUnique(normalizeFooterKeyList(source.footerLine1Keys), seen);
|
|
71
|
-
const footerLine2Keys = takeUnique(normalizeFooterKeyList(source.footerLine2Keys), seen);
|
|
72
|
-
const footerLine3Keys = takeUnique(normalizeFooterKeyList(source.footerLine3Keys), seen);
|
|
73
|
-
const footerHiddenKeys = normalizeFooterKeyList(source.footerHiddenKeys);
|
|
74
|
-
|
|
75
|
-
if (!seen.has(PI_USAGE_KEY)) {
|
|
76
|
-
footerLine1Keys = [PI_USAGE_KEY, ...footerLine1Keys];
|
|
77
|
-
}
|
|
78
138
|
if (!hasHidden && !hasAnyLine) {
|
|
79
|
-
return {
|
|
139
|
+
return {
|
|
140
|
+
footerHiddenKeys: [],
|
|
141
|
+
footerLine1Keys: [...DEFAULT_FOOTER_LINE1_KEYS],
|
|
142
|
+
footerLine2Keys: [...DEFAULT_FOOTER_LINE2_KEYS],
|
|
143
|
+
footerLine3Keys: [],
|
|
144
|
+
};
|
|
80
145
|
}
|
|
81
|
-
|
|
146
|
+
const seen = new Set<string>();
|
|
147
|
+
const line1 = takeUnique(normalizeFooterKeyList(source.footerLine1Keys), seen);
|
|
148
|
+
const line2 = takeUnique(normalizeFooterKeyList(source.footerLine2Keys), seen);
|
|
149
|
+
const line3 = takeUnique(normalizeFooterKeyList(source.footerLine3Keys), seen);
|
|
150
|
+
return {
|
|
151
|
+
footerHiddenKeys: normalizeFooterKeyList(source.footerHiddenKeys),
|
|
152
|
+
...ensureBuiltinKeys(line1, line2, line3),
|
|
153
|
+
};
|
|
82
154
|
}
|
|
83
155
|
|
|
84
|
-
|
|
156
|
+
const BUILTIN_FOOTER_LABEL: Record<string, string> = {
|
|
157
|
+
[FOOTER_MODEL_KEY]: "Model and thinking level",
|
|
158
|
+
[FOOTER_CONTEXT_KEY]: "Context window bar",
|
|
159
|
+
[FOOTER_CACHE_KEY]: "Cache hit rate",
|
|
160
|
+
[FOOTER_RATE_KEY]: "Output rate",
|
|
161
|
+
[FOOTER_COST_KEY]: "Session cost",
|
|
162
|
+
[FOOTER_CWD_KEY]: "Working directory and session name",
|
|
163
|
+
[FOOTER_GIT_KEY]: "Branch and change stats",
|
|
164
|
+
};
|
|
165
|
+
|
|
166
|
+
/** 面板描述用:内置项写明内容,pi-usage 注明来源,其它芯片用来源 setStatus key。 */
|
|
85
167
|
export function footerChipSource(key: string): string {
|
|
86
168
|
return key === PI_USAGE_KEY ? PI_USAGE_SOURCE : key;
|
|
87
169
|
}
|
|
88
170
|
|
|
89
171
|
export function footerChipDescription(key: string, liveText: string): string {
|
|
172
|
+
const builtin = BUILTIN_FOOTER_LABEL[key];
|
|
173
|
+
if (builtin) {
|
|
174
|
+
const live = liveText.trim();
|
|
175
|
+
return live ? `${builtin} · ${live}` : builtin;
|
|
176
|
+
}
|
|
90
177
|
const source = footerChipSource(key);
|
|
91
178
|
const live = liveText.trim();
|
|
92
179
|
if (live) return `${live} · from ${source}`;
|
|
93
|
-
if (key === PI_USAGE_KEY) return `From ${
|
|
180
|
+
if (key === PI_USAGE_KEY) return `From ${PI_USAGE_SOURCE}. No text right now.`;
|
|
94
181
|
return `inactive · from ${source}`;
|
|
95
182
|
}
|
|
96
183
|
|
|
@@ -151,7 +238,6 @@ export function moveFooterKeyToLine(
|
|
|
151
238
|
key: string,
|
|
152
239
|
line: FooterLineId,
|
|
153
240
|
): FooterChipLayout {
|
|
154
|
-
if (isSkippedFooterStatusKey(key)) return layout;
|
|
155
241
|
const current = footerLineOfKey(layout, key);
|
|
156
242
|
if (current === line) return layout;
|
|
157
243
|
const stripped: FooterChipLayout = {
|
|
@@ -193,7 +279,6 @@ export function reorderFooterKey(
|
|
|
193
279
|
}
|
|
194
280
|
|
|
195
281
|
export function toggleFooterKeyHidden(layout: FooterChipLayout, key: string): FooterChipLayout {
|
|
196
|
-
if (isSkippedFooterStatusKey(key)) return layout;
|
|
197
282
|
const hidden = new Set(layout.footerHiddenKeys);
|
|
198
283
|
if (hidden.has(key)) hidden.delete(key);
|
|
199
284
|
else hidden.add(key);
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* 自定义底栏:chips + zentui 图标/句子
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
* 第三行:仅当 line3
|
|
4
|
+
* 默认第一行:model thinking · ██████░░░░ 45%/200k · 42% · $0.01 · pi-usage · 其余 line1 芯片
|
|
5
|
+
* 默认第二行:cwd in session · branch (+16 −1) · 其余 line2 芯片
|
|
6
|
+
* 第三行:仅当 line3 有可见芯片时出现
|
|
7
|
+
* model / context / cache / rate / cost / cwd / git 与插件芯片同一套排布,可换行、调序、隐藏
|
|
7
8
|
*
|
|
8
9
|
* - line1 短芯片,· 分隔;缓存用 zentui ,费用 success
|
|
9
10
|
* - line2 用 zentui 句式 in / on +
|
|
@@ -23,7 +24,15 @@ import { stripAnsi } from "../../utils/ansi-text.ts";
|
|
|
23
24
|
import { formatFooterCost } from "./footer-currency.ts";
|
|
24
25
|
import { formatTokenCount } from "../../utils/format.ts";
|
|
25
26
|
import {
|
|
27
|
+
FOOTER_CACHE_KEY,
|
|
28
|
+
FOOTER_CONTEXT_KEY,
|
|
29
|
+
FOOTER_COST_KEY,
|
|
30
|
+
FOOTER_CWD_KEY,
|
|
31
|
+
FOOTER_GIT_KEY,
|
|
32
|
+
FOOTER_MODEL_KEY,
|
|
33
|
+
FOOTER_RATE_KEY,
|
|
26
34
|
PI_USAGE_KEY,
|
|
35
|
+
isBuiltinFooterKey,
|
|
27
36
|
isSkippedFooterStatusKey,
|
|
28
37
|
resolveFooterChipLayout,
|
|
29
38
|
visibleFooterPluginTexts,
|
|
@@ -31,6 +40,7 @@ import {
|
|
|
31
40
|
} from "./footer-layout.ts";
|
|
32
41
|
import { createGitStatsRefresher, readGitStats, type GitStats } from "./git-stats.ts";
|
|
33
42
|
import { MCP_STATUS_KEY, buildMcpChip } from "./mcp-chip.ts";
|
|
43
|
+
import { inputTextIndent } from "./prompt-editor.ts";
|
|
34
44
|
import { currentTtftMs, displayTokenRate, formatTokenRate } from "./token-rate.ts";
|
|
35
45
|
|
|
36
46
|
export { parseGitStats } from "./git-stats.ts";
|
|
@@ -144,11 +154,11 @@ function ownMcpChip(): string {
|
|
|
144
154
|
return cachedMcpChip && !cachedExtensionStatuses.has(MCP_STATUS_KEY) ? cachedMcpChip : "";
|
|
145
155
|
}
|
|
146
156
|
|
|
147
|
-
/** 面板用:当前 setStatus 文案 + 本包 pi-usage / MCP
|
|
157
|
+
/** 面板用:当前 setStatus 文案 + 本包 pi-usage / MCP(无文案时为空字符串)。内置项由配置保证在列表里。 */
|
|
148
158
|
export function getFooterStatusSnapshot(): Map<string, string> {
|
|
149
159
|
const out = new Map<string, string>();
|
|
150
160
|
for (const [key, text] of cachedExtensionStatuses) {
|
|
151
|
-
if (isSkippedFooterStatusKey(key)) continue;
|
|
161
|
+
if (isSkippedFooterStatusKey(key) || isBuiltinFooterKey(key)) continue;
|
|
152
162
|
out.set(key, text);
|
|
153
163
|
}
|
|
154
164
|
out.set(PI_USAGE_KEY, cachedLocalUsageText);
|
|
@@ -418,21 +428,6 @@ const createCustomFooterFactory =
|
|
|
418
428
|
if (mcpChip) pluginTexts.set(MCP_STATUS_KEY, mcpChip);
|
|
419
429
|
const layout = resolveFooterChipLayout(layoutFromConfig(), [...pluginTexts.keys()]);
|
|
420
430
|
const dimPlugin = (text: string) => colorUsageChip(theme, text);
|
|
421
|
-
const line1Plugins = visibleFooterPluginTexts(
|
|
422
|
-
layout.footerLine1Keys,
|
|
423
|
-
layout.footerHiddenKeys,
|
|
424
|
-
pluginTexts,
|
|
425
|
-
).map(dimPlugin);
|
|
426
|
-
const line2Plugins = visibleFooterPluginTexts(
|
|
427
|
-
layout.footerLine2Keys,
|
|
428
|
-
layout.footerHiddenKeys,
|
|
429
|
-
pluginTexts,
|
|
430
|
-
).map(dimPlugin);
|
|
431
|
-
const line3Plugins = visibleFooterPluginTexts(
|
|
432
|
-
layout.footerLine3Keys,
|
|
433
|
-
layout.footerHiddenKeys,
|
|
434
|
-
pluginTexts,
|
|
435
|
-
).map(dimPlugin);
|
|
436
431
|
|
|
437
432
|
const pctLabel = percent === null || percent === undefined ? "?" : `${Math.floor(percent)}%`;
|
|
438
433
|
const cachePct = getCachePct();
|
|
@@ -447,22 +442,13 @@ const createCustomFooterFactory =
|
|
|
447
442
|
config.footerTokenRateTtft,
|
|
448
443
|
)
|
|
449
444
|
: "";
|
|
450
|
-
const
|
|
451
|
-
const costChip =
|
|
445
|
+
const costLabel =
|
|
452
446
|
cost || usingSubscription
|
|
453
|
-
?
|
|
454
|
-
"dim",
|
|
455
|
-
formatFooterCost(cost, config.footerCurrency, config.footerCurrencyRate),
|
|
456
|
-
) + (usingSubscription ? theme.fg("warning", " sub") : "")
|
|
447
|
+
? formatFooterCost(cost, config.footerCurrency, config.footerCurrencyRate)
|
|
457
448
|
: "";
|
|
458
|
-
const
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
cacheLabel ? theme.fg("dim", cacheLabel) : "",
|
|
462
|
-
rateChip,
|
|
463
|
-
costChip,
|
|
464
|
-
...line1Plugins,
|
|
465
|
-
]);
|
|
449
|
+
const costChip = costLabel
|
|
450
|
+
? theme.fg("dim", costLabel) + (usingSubscription ? theme.fg("warning", " sub") : "")
|
|
451
|
+
: "";
|
|
466
452
|
|
|
467
453
|
const gitColor = theme.getThinkingBorderColor("medium");
|
|
468
454
|
let place = theme.fg("accent", cwdLabel);
|
|
@@ -471,6 +457,7 @@ const createCustomFooterFactory =
|
|
|
471
457
|
place += theme.fg("muted", " in ") + theme.fg("success", sessionName);
|
|
472
458
|
}
|
|
473
459
|
const branch = footerData.getGitBranch();
|
|
460
|
+
let gitChip = "";
|
|
474
461
|
if (branch) {
|
|
475
462
|
const s = gitStats;
|
|
476
463
|
let stats = "";
|
|
@@ -484,16 +471,45 @@ const createCustomFooterFactory =
|
|
|
484
471
|
theme.fg("error", `−${s.del}`) +
|
|
485
472
|
theme.fg("dim", ")");
|
|
486
473
|
const gitLabel = glyphs.git ? `${glyphs.git} ${branch}` : branch;
|
|
487
|
-
|
|
474
|
+
gitChip = gitColor(gitLabel) + stats;
|
|
488
475
|
}
|
|
489
|
-
|
|
490
|
-
const
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
476
|
+
|
|
477
|
+
const builtinChips = new Map<string, string>([
|
|
478
|
+
[
|
|
479
|
+
FOOTER_MODEL_KEY,
|
|
480
|
+
modelChip(theme, modelLabel, model?.reasoning ? thinkingLevelStr : "", thinkingColor),
|
|
481
|
+
],
|
|
482
|
+
[
|
|
483
|
+
FOOTER_CONTEXT_KEY,
|
|
484
|
+
barGauge(percent ?? 0) + theme.fg("dim", ` ${pctLabel}/${fmt(contextWindow)}`),
|
|
485
|
+
],
|
|
486
|
+
[FOOTER_CACHE_KEY, cacheLabel ? theme.fg("dim", cacheLabel) : ""],
|
|
487
|
+
[FOOTER_RATE_KEY, rateLabel ? theme.fg("dim", rateLabel) : ""],
|
|
488
|
+
[FOOTER_COST_KEY, costChip],
|
|
489
|
+
[FOOTER_CWD_KEY, place],
|
|
490
|
+
[FOOTER_GIT_KEY, gitChip],
|
|
491
|
+
]);
|
|
492
|
+
|
|
493
|
+
const lineOf = (keys: readonly string[]) =>
|
|
494
|
+
joinChips(
|
|
495
|
+
keys
|
|
496
|
+
.filter((key) => !layout.footerHiddenKeys.includes(key))
|
|
497
|
+
.map((key) => {
|
|
498
|
+
if (isBuiltinFooterKey(key)) return builtinChips.get(key) ?? "";
|
|
499
|
+
const text = visibleFooterPluginTexts([key], [], pluginTexts)[0];
|
|
500
|
+
return text ? dimPlugin(text) : "";
|
|
501
|
+
}),
|
|
502
|
+
);
|
|
503
|
+
const line1 = lineOf(layout.footerLine1Keys);
|
|
504
|
+
const line2 = lineOf(layout.footerLine2Keys);
|
|
505
|
+
const line3 = lineOf(layout.footerLine3Keys);
|
|
506
|
+
|
|
507
|
+
// 对齐输入文本起始列:editorPaddingX + `❯ ` 的 2 列(随编辑器 render 实时更新)。
|
|
508
|
+
const indent = " ".repeat(inputTextIndent());
|
|
509
|
+
const fit = (line: string) =>
|
|
510
|
+
indent + truncateToWidth(line, Math.max(1, width - indent.length));
|
|
511
|
+
|
|
512
|
+
return [fit(line1), ...(line2 ? [fit(line2)] : []), ...(line3 ? [fit(line3)] : [])];
|
|
497
513
|
};
|
|
498
514
|
|
|
499
515
|
return {
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 输入框 `❯` 提示符(issue #68)。
|
|
3
|
+
*
|
|
4
|
+
* `PromptEditor` 继承 `CustomEditor`:pi 的 setCustomEditorComponent 会自动接线
|
|
5
|
+
* onSubmit/onChange/actionHandlers/autocomplete/工作状态指示器,本扩展只管渲染。
|
|
6
|
+
*
|
|
7
|
+
* `❯ ` 恒占左侧 2 列:渲染前把 paddingX 抬到「用户值 + 2」(直接改字段绕过
|
|
8
|
+
* setter,避免每帧 requestRender 自激),渲染后恢复。首内容行前两列换成
|
|
9
|
+
* `❯ `,其后仍保留用户 editorPaddingX 的空格。
|
|
10
|
+
* `❯` 颜色直接用 `borderColor`,随 thinking 等级 / bash mode 自动联动。
|
|
11
|
+
*
|
|
12
|
+
* `userPaddingX` 记录渲染时读到的用户 editorPaddingX(pi 通过 setPaddingX
|
|
13
|
+
* 同步进本编辑器),footer 用 `inputTextIndent()` 对齐输入文本起始列:
|
|
14
|
+
* 前缀开 = userPaddingX + 2,关 = userPaddingX。
|
|
15
|
+
*/
|
|
16
|
+
import { CustomEditor, type ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
17
|
+
import { config } from "../../config/config.ts";
|
|
18
|
+
|
|
19
|
+
/** `❯ ` 固定 2 列:glyph + 尾随空格。 */
|
|
20
|
+
const PROMPT_GLYPH = "❯";
|
|
21
|
+
const PROMPT_COLS = 2;
|
|
22
|
+
|
|
23
|
+
/** 最近一次 render 读到的用户 editorPaddingX;未启用过提示符时为 0。 */
|
|
24
|
+
let userPaddingX = 0;
|
|
25
|
+
|
|
26
|
+
/** 输入文本起始列:footer 按它对齐。 */
|
|
27
|
+
export function inputTextIndent(): number {
|
|
28
|
+
return config.enableEditorPrefix ? userPaddingX + PROMPT_COLS : userPaddingX;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export class PromptEditor extends CustomEditor {
|
|
32
|
+
render(width: number): string[] {
|
|
33
|
+
// ❯ + 用户 padding:文本从 userPaddingX+2 列开始。
|
|
34
|
+
const self = this as unknown as { paddingX: number };
|
|
35
|
+
const saved = self.paddingX;
|
|
36
|
+
userPaddingX = saved;
|
|
37
|
+
self.paddingX = saved + PROMPT_COLS;
|
|
38
|
+
try {
|
|
39
|
+
const lines = super.render(width);
|
|
40
|
+
// 布局:lines[0] 顶边框,lines[1..n-2] 内容,lines[n-1] 底边框(autocomplete 在底边框后追加,
|
|
41
|
+
// 首内容行恒为 index 1)。开头是 paddingX 个纯空格,前两列换成 ❯+空格,剩余 padding 保留;
|
|
42
|
+
// 续行不动——Claude Code 行为。
|
|
43
|
+
if (lines.length > 2 && lines[1].startsWith(" ")) {
|
|
44
|
+
lines[1] = this.borderColor(PROMPT_GLYPH) + " " + lines[1].slice(2);
|
|
45
|
+
}
|
|
46
|
+
return lines;
|
|
47
|
+
} finally {
|
|
48
|
+
self.paddingX = saved;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
type EditorFactory = Parameters<ExtensionContext["ui"]["setEditorComponent"]>[0];
|
|
54
|
+
|
|
55
|
+
const promptEditorFactory: EditorFactory = (tui, theme, keybindings) =>
|
|
56
|
+
new PromptEditor(tui, theme, keybindings, { embedWorkingStatus: true });
|
|
57
|
+
|
|
58
|
+
/** 是否正占着插槽;占据前的工厂用于原样还回去。 */
|
|
59
|
+
let active = false;
|
|
60
|
+
let previousEditorFactory: EditorFactory | undefined;
|
|
61
|
+
|
|
62
|
+
/** 撤销本扩展占据的插槽;从未占用就不碰插槽。 */
|
|
63
|
+
function releasePromptEditor(ctx: ExtensionContext): void {
|
|
64
|
+
if (!active) return;
|
|
65
|
+
ctx.ui.setEditorComponent(previousEditorFactory);
|
|
66
|
+
active = false;
|
|
67
|
+
previousEditorFactory = undefined;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/** 应用输入框前缀;enableEditorPrefix=false 时只撤自己装的那层。 */
|
|
71
|
+
export function applyPromptEditor(ctx: ExtensionContext): void {
|
|
72
|
+
if (!ctx?.hasUI || typeof ctx.ui?.setEditorComponent !== "function") return;
|
|
73
|
+
try {
|
|
74
|
+
if (!config.enableEditorPrefix) {
|
|
75
|
+
releasePromptEditor(ctx);
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
if (!active) {
|
|
79
|
+
// pi 的插槽是硬替换、没有栈,先记住是谁占着,撤销时还回去。
|
|
80
|
+
previousEditorFactory = ctx.ui.getEditorComponent?.();
|
|
81
|
+
active = true;
|
|
82
|
+
}
|
|
83
|
+
ctx.ui.setEditorComponent(promptEditorFactory);
|
|
84
|
+
} catch (err) {
|
|
85
|
+
ctx.ui.notify(
|
|
86
|
+
`prompt editor error: ${err instanceof Error ? err.message : String(err)}`,
|
|
87
|
+
"error",
|
|
88
|
+
);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/** 恢复本扩展占据前的编辑器;未占用时不动作。 */
|
|
93
|
+
export function clearPromptEditor(ctx: ExtensionContext): void {
|
|
94
|
+
if (!ctx?.hasUI || typeof ctx.ui?.setEditorComponent !== "function") return;
|
|
95
|
+
releasePromptEditor(ctx);
|
|
96
|
+
}
|
package/extensions/index.ts
CHANGED
|
@@ -5,6 +5,7 @@ import { config } from "./config/config.ts";
|
|
|
5
5
|
import piAliases from "./feature/shell/aliases.ts";
|
|
6
6
|
import { installFlushDockedBash } from "./feature/shell/flush-docked-bash.ts";
|
|
7
7
|
import customFooter from "./feature/shell/footer.ts";
|
|
8
|
+
import { applyPromptEditor } from "./feature/shell/prompt-editor.ts";
|
|
8
9
|
import piStartupHeader, { installEarlyStartupHeader } from "./feature/shell/startup-header.ts";
|
|
9
10
|
import workingMessage from "./feature/shell/working-message.ts";
|
|
10
11
|
|
|
@@ -28,6 +29,11 @@ export default function (pi: ExtensionAPI): void {
|
|
|
28
29
|
if (config.enableWorkingMessage) workingMessage(pi);
|
|
29
30
|
customFooter(pi);
|
|
30
31
|
|
|
32
|
+
// 输入框 ❯ 提示符:session_start 时按配置应用;setEditorComponent 是一次性替换。
|
|
33
|
+
pi.on("session_start", (_event, ctx) => {
|
|
34
|
+
applyPromptEditor(ctx);
|
|
35
|
+
});
|
|
36
|
+
|
|
31
37
|
// render stack:thinking controller 直接交给 style 作 query
|
|
32
38
|
markdownEnhance(pi);
|
|
33
39
|
claudeCodeStyle(pi, undefined, installCompactThinking(pi, getCompactThinkingConfig()));
|