pi-cc-loop 0.6.0 → 0.7.1
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 +21 -17
- package/config.ts +23 -5
- package/cron.ts +10 -20
- package/index.ts +2 -2
- package/loopfile.ts +1 -1
- package/package.json +1 -1
- package/render.ts +0 -9
- package/settings.ts +151 -63
package/README.md
CHANGED
|
@@ -38,7 +38,7 @@ ln -sfn "$PWD" ~/.pi/agent/extensions/pi-cc-loop
|
|
|
38
38
|
| 次数上限 | `maxFires`,到顶自动删除 | `--max N` |
|
|
39
39
|
| 持久化 | `durable: true` 写入 `.pi/scheduled_tasks.json`,重启后继续;同一项目只由一个 Pi 会话执行 | `--durable` |
|
|
40
40
|
| 底部摘要 | Pi 底部状态栏一行:`loop 2 · next 3m`,无任务时不占位 | `/pi_cc footer on\|off` |
|
|
41
|
-
| 界面设置 |
|
|
41
|
+
| 界面设置 | 按插件分页的设置面板,本地命令,不进对话 | `/pi_cc` |
|
|
42
42
|
|
|
43
43
|
## 命令
|
|
44
44
|
|
|
@@ -86,43 +86,47 @@ Ctrl+O 展开后多一行元信息(任务 id、节奏)。`compactMessages`
|
|
|
86
86
|
|
|
87
87
|
## 设置
|
|
88
88
|
|
|
89
|
-
|
|
89
|
+
四个 pi-cc 扩展共用 Pi 配置目录下的 `pi-cc.json`(默认 `~/.pi/agent/pi-cc.json`,跟随 `PI_CODING_AGENT_DIR`),文件不存在时用默认值:
|
|
90
90
|
|
|
91
91
|
```jsonc
|
|
92
92
|
{
|
|
93
93
|
"compactMessages": true, // 触发块和 Monitor 事件用上面的样式;false 为旧的带底色卡片
|
|
94
94
|
"footer": true, // 底部摘要:loop 的一行和 Monitor 的 `monitor N`
|
|
95
95
|
"draft": true, // pi-cc-plugins:Ctrl+C 找回
|
|
96
|
-
"turnSummary": true
|
|
96
|
+
"turnSummary": true, // pi-cc-plugins:回合摘要一行
|
|
97
|
+
"compaction": true, // pi-cc-compaction:Claude Code 的压缩
|
|
98
|
+
"idleCompaction": true // pi-cc-compaction:空闲压缩
|
|
97
99
|
}
|
|
98
100
|
```
|
|
99
101
|
|
|
100
|
-
|
|
102
|
+
draft、turnSummary 只被 pi-cc-plugins 读取,compaction、idleCompaction 只被 pi-cc-compaction 读取;没装对应包的会话里,面板中的这些项不生效,值原样保留。
|
|
101
103
|
|
|
102
|
-
在 TUI 里输入 `/pi_cc`
|
|
103
|
-
|
|
104
|
+
在 TUI 里输入 `/pi_cc` 会打开设置面板,和 pi-cc-extensions 的 `/ccstyle` 一样按页组织,每个插件一页:
|
|
105
|
+
**Loop & Monitor**(消息样式、底部摘要,两个包共用)、**Plugins**(pi-cc-plugins)、**Compact**(pi-cc-compaction)。
|
|
106
|
+
Tab / Shift+Tab 切换页,上下键选择,Enter/空格切换,Esc 关闭;改动立即保存,所有 pi-cc 扩展同时生效。
|
|
107
|
+
Pi 的 `/settings` 不接受扩展加项,所以 pi-cc 的设置放在 `/pi_cc` 下。
|
|
104
108
|
|
|
105
|
-
`/pi_cc`
|
|
109
|
+
`/pi_cc` 由四个 pi-cc 包(loop、monitor、plugins、compact)共同提供:装了其中任意一个,命令就在;装了多个,也只有一个 `/pi_cc`
|
|
106
110
|
(Pi 会把不同扩展的同名命令变成 `/pi_cc:1`、`/pi_cc:2`,所以由最先加载的包认领注册,会话结束时释放,`/reload` 会重新认领)。
|
|
107
111
|
|
|
108
112
|
```text
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
→ Message style
|
|
113
|
-
Footer summary
|
|
113
|
+
──────────────────────────────────────────────────────────────────────
|
|
114
|
+
Loop & Monitor / Plugins / Compact ~/.pi/agent/pi-cc.json
|
|
115
|
+
──────────────────────────────────────────────────────────────────────
|
|
116
|
+
→ Message style compact
|
|
117
|
+
Footer summary on
|
|
114
118
|
|
|
115
119
|
How scheduled fires and Monitor events are drawn. compact: ✻ header
|
|
116
120
|
and indented body; card: the old padded card.
|
|
117
|
-
|
|
118
|
-
Enter/Space to change · Esc to
|
|
119
|
-
|
|
121
|
+
──────────────────────────────────────────────────────────────────────
|
|
122
|
+
Enter/Space to change · Tab/Shift+Tab to switch pages · Esc to close
|
|
123
|
+
──────────────────────────────────────────────────────────────────────
|
|
120
124
|
```
|
|
121
125
|
|
|
122
126
|
也可以用文字命令:`/pi_cc messages on|off`、`/pi_cc footer on|off`,适合脚本和 RPC;
|
|
123
|
-
非 TUI 模式下,不带参数的 `/pi_cc`
|
|
127
|
+
非 TUI 模式下,不带参数的 `/pi_cc` 按页输出当前设置。命令只写改动的那一项,文件里的其他内容原样保留。
|
|
124
128
|
|
|
125
|
-
环境变量 `PI_CC_FOOTER`、`PI_CC_COMPACT_MESSAGES`(pi-cc-plugins 的两项是 `PI_CC_DRAFT`、`PI_CC_TURN_SUMMARY`)优先于文件,但不会被写进文件。设着的时候,面板里这一项显示实际生效的值、不能切换,
|
|
129
|
+
环境变量 `PI_CC_FOOTER`、`PI_CC_COMPACT_MESSAGES`(pi-cc-plugins 的两项是 `PI_CC_DRAFT`、`PI_CC_TURN_SUMMARY`,pi-cc-compaction 的两项是 `PI_CC_COMPACTION`、`PI_CC_IDLE_COMPACTION`)优先于文件,但不会被写进文件。设着的时候,面板里这一项显示实际生效的值、不能切换,
|
|
126
130
|
说明里写出是哪个变量;文字输出会在对应项后面标出来(如 `hidden (PI_CC_FOOTER=0)`);用文字命令切换会写进文件,但提示会说明变量还在覆盖它。
|
|
127
131
|
|
|
128
132
|
## 底部摘要
|
package/config.ts
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Shared settings for the pi-cc extensions.
|
|
3
3
|
*
|
|
4
|
-
* pi-cc-loop, pi-cc-monitor and pi-cc-
|
|
5
|
-
* so a single toggle changes the whole family at once. The file is optional;
|
|
4
|
+
* pi-cc-loop, pi-cc-monitor, pi-cc-plugins and pi-cc-compaction read the same
|
|
5
|
+
* small JSON file so a single toggle changes the whole family at once. The file is optional;
|
|
6
6
|
* when it is missing or malformed the defaults apply. It sits in Pi's agent
|
|
7
7
|
* directory (PI_CODING_AGENT_DIR, default ~/.pi/agent), next to Pi's own
|
|
8
8
|
* settings.
|
|
@@ -12,11 +12,13 @@
|
|
|
12
12
|
* "compactMessages": true,
|
|
13
13
|
* "footer": true,
|
|
14
14
|
* "draft": true,
|
|
15
|
-
* "turnSummary": true
|
|
15
|
+
* "turnSummary": true,
|
|
16
|
+
* "compaction": true,
|
|
17
|
+
* "idleCompaction": true
|
|
16
18
|
* }
|
|
17
19
|
*
|
|
18
20
|
* compactMessages and footer drive loop and monitor; draft and turnSummary
|
|
19
|
-
* drive pi-cc-plugins. A package that is not installed simply ignores its
|
|
21
|
+
* drive pi-cc-plugins; compaction and idleCompaction drive pi-cc-compaction. A package that is not installed simply ignores its
|
|
20
22
|
* keys, and they are kept as the user left them.
|
|
21
23
|
*
|
|
22
24
|
* Environment overrides (useful for tests and quick experiments) win over the
|
|
@@ -26,6 +28,8 @@
|
|
|
26
28
|
* PI_CC_FOOTER=0|1
|
|
27
29
|
* PI_CC_DRAFT=0|1
|
|
28
30
|
* PI_CC_TURN_SUMMARY=0|1
|
|
31
|
+
* PI_CC_COMPACTION=0|1
|
|
32
|
+
* PI_CC_IDLE_COMPACTION=0|1
|
|
29
33
|
*/
|
|
30
34
|
|
|
31
35
|
import { getAgentDir } from "@earendil-works/pi-coding-agent";
|
|
@@ -44,6 +48,10 @@ export interface PiCcConfig {
|
|
|
44
48
|
draft: boolean;
|
|
45
49
|
/** pi-cc-plugins: one line closes every agent run. */
|
|
46
50
|
turnSummary: boolean;
|
|
51
|
+
/** pi-cc-compaction: Claude Code's compaction replaces Pi's summary request. */
|
|
52
|
+
compaction: boolean;
|
|
53
|
+
/** pi-cc-compaction: compact while idle, before a long prompt cache expires. */
|
|
54
|
+
idleCompaction: boolean;
|
|
47
55
|
}
|
|
48
56
|
|
|
49
57
|
export const DEFAULT_CONFIG: PiCcConfig = {
|
|
@@ -51,6 +59,8 @@ export const DEFAULT_CONFIG: PiCcConfig = {
|
|
|
51
59
|
footer: true,
|
|
52
60
|
draft: true,
|
|
53
61
|
turnSummary: true,
|
|
62
|
+
compaction: true,
|
|
63
|
+
idleCompaction: true,
|
|
54
64
|
};
|
|
55
65
|
|
|
56
66
|
/** The environment variable that overrides each setting. */
|
|
@@ -59,6 +69,8 @@ const ENV_OVERRIDES: Record<keyof PiCcConfig, string> = {
|
|
|
59
69
|
footer: "PI_CC_FOOTER",
|
|
60
70
|
draft: "PI_CC_DRAFT",
|
|
61
71
|
turnSummary: "PI_CC_TURN_SUMMARY",
|
|
72
|
+
compaction: "PI_CC_COMPACTION",
|
|
73
|
+
idleCompaction: "PI_CC_IDLE_COMPACTION",
|
|
62
74
|
};
|
|
63
75
|
|
|
64
76
|
function envBool(name: string): boolean | undefined {
|
|
@@ -75,7 +87,7 @@ export function envOverride(key: keyof PiCcConfig): string | undefined {
|
|
|
75
87
|
return envBool(name) === undefined ? undefined : `${name}=${process.env[name]}`;
|
|
76
88
|
}
|
|
77
89
|
|
|
78
|
-
/** The settings file shared by pi-cc
|
|
90
|
+
/** The settings file shared by the pi-cc packages, in Pi's agent directory. */
|
|
79
91
|
export function configPath(): string {
|
|
80
92
|
if (process.env.PI_CC_CONFIG_FILE) return process.env.PI_CC_CONFIG_FILE;
|
|
81
93
|
return path.join(getAgentDir(), "pi-cc.json");
|
|
@@ -99,6 +111,12 @@ function readConfig(): PiCcConfig {
|
|
|
99
111
|
turnSummary:
|
|
100
112
|
envBool(ENV_OVERRIDES.turnSummary) ??
|
|
101
113
|
(typeof file.turnSummary === "boolean" ? file.turnSummary : DEFAULT_CONFIG.turnSummary),
|
|
114
|
+
compaction:
|
|
115
|
+
envBool(ENV_OVERRIDES.compaction) ??
|
|
116
|
+
(typeof file.compaction === "boolean" ? file.compaction : DEFAULT_CONFIG.compaction),
|
|
117
|
+
idleCompaction:
|
|
118
|
+
envBool(ENV_OVERRIDES.idleCompaction) ??
|
|
119
|
+
(typeof file.idleCompaction === "boolean" ? file.idleCompaction : DEFAULT_CONFIG.idleCompaction),
|
|
102
120
|
};
|
|
103
121
|
}
|
|
104
122
|
|
package/cron.ts
CHANGED
|
@@ -4,8 +4,9 @@
|
|
|
4
4
|
* Fields: minute hour day-of-month month day-of-week
|
|
5
5
|
* Per-field syntax: `*`, `*\/N`, `a`, `a-b`, `a-b/N`, `a,b,c`
|
|
6
6
|
*
|
|
7
|
-
*
|
|
8
|
-
* and
|
|
7
|
+
* The subset Claude Code 2.1.308 accepts, nothing more: a step after a
|
|
8
|
+
* single value (`5/15`) is invalid there too, and `L`, `W`, `?` and name
|
|
9
|
+
* aliases such as `MON` are not supported.
|
|
9
10
|
*/
|
|
10
11
|
|
|
11
12
|
export interface CronFields {
|
|
@@ -14,9 +15,9 @@ export interface CronFields {
|
|
|
14
15
|
dom: Set<number>;
|
|
15
16
|
month: Set<number>;
|
|
16
17
|
dow: Set<number>;
|
|
17
|
-
/** True when the day-of-month field
|
|
18
|
+
/** True when the day-of-month field leaves days out (Claude Code: fewer than 31 values). */
|
|
18
19
|
domRestricted: boolean;
|
|
19
|
-
/** True when the day-of-week field
|
|
20
|
+
/** True when the day-of-week field leaves days out (Claude Code: fewer than 7 values). */
|
|
20
21
|
dowRestricted: boolean;
|
|
21
22
|
}
|
|
22
23
|
|
|
@@ -59,6 +60,8 @@ function parseField(spec: FieldSpec, raw: string): Set<number> | null {
|
|
|
59
60
|
} else {
|
|
60
61
|
const m = /^(\d+)(?:-(\d+))?$/.exec(rangePart);
|
|
61
62
|
if (!m) return null;
|
|
63
|
+
// "5/15": Claude Code rejects a step after a single value.
|
|
64
|
+
if (stepPart !== undefined && m[2] === undefined) return null;
|
|
62
65
|
start = Number(m[1]);
|
|
63
66
|
end = m[2] !== undefined ? Number(m[2]) : start;
|
|
64
67
|
if (end < start) return null;
|
|
@@ -93,8 +96,9 @@ export function parseCron(expression: string): CronFields | null {
|
|
|
93
96
|
dom: parsed[2],
|
|
94
97
|
month: parsed[3],
|
|
95
98
|
dow: parsed[4],
|
|
96
|
-
|
|
97
|
-
|
|
99
|
+
// Claude Code compares with the full range, so 1-31 or */1 is every day.
|
|
100
|
+
domRestricted: parsed[2].size < 31,
|
|
101
|
+
dowRestricted: parsed[4].size < 7,
|
|
98
102
|
};
|
|
99
103
|
}
|
|
100
104
|
|
|
@@ -233,17 +237,3 @@ function clockText(minute: number, hour: number): string {
|
|
|
233
237
|
const display = hour % 12 === 0 ? 12 : hour % 12;
|
|
234
238
|
return `${display}:${String(minute).padStart(2, "0")} ${suffix}`;
|
|
235
239
|
}
|
|
236
|
-
|
|
237
|
-
/** Human readable summary of a cron expression. */
|
|
238
|
-
export function describeCron(expression: string): string {
|
|
239
|
-
const fields = parseCron(expression);
|
|
240
|
-
if (!fields) return expression;
|
|
241
|
-
const [minute, hour, dom, month, dow] = expression.trim().split(/\s+/);
|
|
242
|
-
const parts: string[] = [];
|
|
243
|
-
if (minute !== "*") parts.push(`minute ${minute}`);
|
|
244
|
-
if (hour !== "*") parts.push(`hour ${hour}`);
|
|
245
|
-
if (dom !== "*") parts.push(`day-of-month ${dom}`);
|
|
246
|
-
if (month !== "*") parts.push(`month ${month}`);
|
|
247
|
-
if (dow !== "*") parts.push(`day-of-week ${dow}`);
|
|
248
|
-
return parts.length ? parts.join(", ") : "every minute";
|
|
249
|
-
}
|
package/index.ts
CHANGED
|
@@ -86,7 +86,7 @@ const LOOP_STOPPED_CHANNEL = "loop:stopped";
|
|
|
86
86
|
const MAX_JOBS = 50;
|
|
87
87
|
const MIN_DELAY_SECONDS = 60;
|
|
88
88
|
const MAX_DELAY_SECONDS = 3600;
|
|
89
|
-
/**
|
|
89
|
+
/** The one fallback wakeup a self-paced chain gets when the model forgot to reschedule. */
|
|
90
90
|
const DEFAULT_KEEPALIVE_SECONDS = 1200;
|
|
91
91
|
const DEFAULT_MAX_AGE_DAYS = 7;
|
|
92
92
|
/** How many stopped prompts are remembered (and persisted). */
|
|
@@ -628,7 +628,7 @@ export default function piCcLoopExtension(pi: ExtensionAPI): void {
|
|
|
628
628
|
"/loop [interval] <prompt> run <prompt> every <interval> (e.g. 30s, 5m, 2h, 1d)",
|
|
629
629
|
"/loop <prompt> self-paced: the agent picks the next delay",
|
|
630
630
|
"/loop [interval] the .pi/loop.md tasks, or the built-in maintenance prompt",
|
|
631
|
-
"/pi_cc pi-cc settings
|
|
631
|
+
"/pi_cc pi-cc settings, one page per plugin",
|
|
632
632
|
"",
|
|
633
633
|
"Esc on a self-paced loop's own turn cancels that loop; fixed-interval loops keep running.",
|
|
634
634
|
"Flags: --durable write the job to .pi/scheduled_tasks.json so it survives a restart.",
|
package/loopfile.ts
CHANGED
|
@@ -65,7 +65,7 @@ export type TickMode = "cron" | "wakeup";
|
|
|
65
65
|
export class LoopFileDelivery {
|
|
66
66
|
private lastDelivered: string | null = null;
|
|
67
67
|
|
|
68
|
-
/** Send the full text on the next tick
|
|
68
|
+
/** Send the full text on the next tick: after a compaction the earlier copy is gone. */
|
|
69
69
|
reset(): void {
|
|
70
70
|
this.lastDelivered = null;
|
|
71
71
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-cc-loop",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.7.1",
|
|
4
4
|
"description": "Claude Code compatible /loop for the Pi coding agent: cron-scheduled fixed intervals, self-paced chains, and the same dispatch semantics as Claude Code.",
|
|
5
5
|
"author": "tarnish233",
|
|
6
6
|
"type": "module",
|
package/render.ts
CHANGED
|
@@ -57,15 +57,6 @@ export function formatDuration(ms: number): string {
|
|
|
57
57
|
return rest ? `${minutes}m ${rest}s` : `${minutes}m`;
|
|
58
58
|
}
|
|
59
59
|
|
|
60
|
-
/** The first non-empty line of a text block. */
|
|
61
|
-
export function firstLine(text: string): string {
|
|
62
|
-
for (const line of text.split("\n")) {
|
|
63
|
-
const trimmed = line.trim();
|
|
64
|
-
if (trimmed) return trimmed;
|
|
65
|
-
}
|
|
66
|
-
return "";
|
|
67
|
-
}
|
|
68
|
-
|
|
69
60
|
/** Split a text block into displayable lines, dropping blank ones. */
|
|
70
61
|
export function bodyLines(text: string): string[] {
|
|
71
62
|
return text
|
package/settings.ts
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* The /pi_cc command: the settings every pi-cc package shares.
|
|
3
3
|
*
|
|
4
|
-
* This file is copied verbatim between pi-cc-loop, pi-cc-monitor
|
|
5
|
-
* pi-cc-plugins (separate npm packages); each repo's tests/shared.test.mjs
|
|
4
|
+
* This file is copied verbatim between pi-cc-loop, pi-cc-monitor,
|
|
5
|
+
* pi-cc-plugins and pi-cc-compaction (separate npm packages); each repo's tests/shared.test.mjs
|
|
6
6
|
* fails when the copies drift apart. The settings live in one pi-cc.json and
|
|
7
7
|
* apply to every pi-cc package installed. A change is announced on the
|
|
8
8
|
* "pi-cc:config" channel, so the packages repaint what the settings change
|
|
@@ -10,12 +10,15 @@
|
|
|
10
10
|
*
|
|
11
11
|
* Pi's /settings cannot take items from an extension (its panel is a fixed
|
|
12
12
|
* list, and the command is handled before extension commands), so /pi_cc
|
|
13
|
-
* opens
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
13
|
+
* opens its own, laid out like pi-cc-extensions' /ccstyle: one page per
|
|
14
|
+
* plugin, a tab bar on top (Tab and Shift+Tab switch pages), and on each page
|
|
15
|
+
* Pi's SettingsList with the selected setting's description below it.
|
|
16
|
+
* Enter/Space changes a value; a change is saved at once and Esc closes.
|
|
17
|
+
* Message style and the footer summary serve pi-cc-loop and pi-cc-monitor
|
|
18
|
+
* alike, so the two share a page. A setting that an environment variable
|
|
19
|
+
* overrides shows the value in effect and cannot be changed here; its
|
|
20
|
+
* description names the variable. Outside the TUI, /pi_cc prints the summary,
|
|
21
|
+
* page by page.
|
|
19
22
|
*
|
|
20
23
|
* Only one /pi_cc exists however many pi-cc packages are installed: Pi turns
|
|
21
24
|
* same-named commands from different extensions into /pi_cc:1 and /pi_cc:2,
|
|
@@ -27,8 +30,8 @@
|
|
|
27
30
|
* getCommands() cannot be called during loading.
|
|
28
31
|
*/
|
|
29
32
|
|
|
30
|
-
import {
|
|
31
|
-
import {
|
|
33
|
+
import type { ExtensionAPI, ExtensionCommandContext, ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
34
|
+
import { matchesKey, SettingsList, truncateToWidth, visibleWidth, type SettingItem, type SettingsListTheme } from "@earendil-works/pi-tui";
|
|
32
35
|
import os from "node:os";
|
|
33
36
|
import path from "node:path";
|
|
34
37
|
import { configPath, envOverride, loadConfig, saveConfig, type PiCcConfig } from "./config.ts";
|
|
@@ -54,42 +57,83 @@ interface SettingSpec {
|
|
|
54
57
|
description: string;
|
|
55
58
|
}
|
|
56
59
|
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
description:
|
|
64
|
-
"How scheduled fires and Monitor events are drawn. compact: \u273b header and indented body; card: the old padded card.",
|
|
65
|
-
},
|
|
60
|
+
interface SettingsPage {
|
|
61
|
+
label: string;
|
|
62
|
+
settings: SettingSpec[];
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const PAGES: SettingsPage[] = [
|
|
66
66
|
{
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
67
|
+
label: "Loop & Monitor",
|
|
68
|
+
settings: [
|
|
69
|
+
{
|
|
70
|
+
field: "compactMessages",
|
|
71
|
+
label: "Message style",
|
|
72
|
+
on: "compact",
|
|
73
|
+
off: "card",
|
|
74
|
+
description:
|
|
75
|
+
"How scheduled fires and Monitor events are drawn. compact: \u273b header and indented body; card: the old padded card.",
|
|
76
|
+
},
|
|
77
|
+
{
|
|
78
|
+
field: "footer",
|
|
79
|
+
label: "Footer summary",
|
|
80
|
+
on: "on",
|
|
81
|
+
off: "off",
|
|
82
|
+
description: "One line in Pi's footer while loops or monitors run: loop 2 \u00b7 next 3m, monitor 2.",
|
|
83
|
+
},
|
|
84
|
+
],
|
|
72
85
|
},
|
|
73
86
|
{
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
87
|
+
label: "Plugins",
|
|
88
|
+
settings: [
|
|
89
|
+
{
|
|
90
|
+
field: "draft",
|
|
91
|
+
label: "Draft recall",
|
|
92
|
+
on: "on",
|
|
93
|
+
off: "off",
|
|
94
|
+
description: "Ctrl+C keeps what it clears; Up on the empty editor brings it back.",
|
|
95
|
+
},
|
|
96
|
+
{
|
|
97
|
+
field: "turnSummary",
|
|
98
|
+
label: "Turn summary",
|
|
99
|
+
on: "on",
|
|
100
|
+
off: "off",
|
|
101
|
+
description: "One line closes every agent run \u2014 tools, duration, done time.",
|
|
102
|
+
},
|
|
103
|
+
],
|
|
79
104
|
},
|
|
80
105
|
{
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
106
|
+
label: "Compact",
|
|
107
|
+
settings: [
|
|
108
|
+
{
|
|
109
|
+
field: "compaction",
|
|
110
|
+
label: "CC compaction",
|
|
111
|
+
on: "on",
|
|
112
|
+
off: "off",
|
|
113
|
+
description:
|
|
114
|
+
"Compaction summarizes the way Claude Code does \u2014 the same prompt and thresholds, reusing the prompt cache. off: Pi's own compaction.",
|
|
115
|
+
},
|
|
116
|
+
{
|
|
117
|
+
field: "idleCompaction",
|
|
118
|
+
label: "Idle compaction",
|
|
119
|
+
on: "on",
|
|
120
|
+
off: "off",
|
|
121
|
+
description:
|
|
122
|
+
"Compact while idle, before a prompt cache of an hour or more expires (Anthropic's 1h cache, or a model's promptCache).",
|
|
123
|
+
},
|
|
124
|
+
],
|
|
86
125
|
},
|
|
87
126
|
];
|
|
88
127
|
|
|
128
|
+
const SETTINGS: SettingSpec[] = PAGES.flatMap((page) => page.settings);
|
|
129
|
+
|
|
130
|
+
/** Labels padded to one width, so the values stay in one column from page to page. */
|
|
131
|
+
const LABEL_WIDTH = Math.max(...SETTINGS.map((spec) => spec.label.length));
|
|
132
|
+
|
|
89
133
|
/**
|
|
90
134
|
* Pi's getSettingsListTheme(), built from the theme the panel is given: the
|
|
91
135
|
* global theme that function reads is not shared with extensions in every
|
|
92
|
-
* Pi runtime
|
|
136
|
+
* Pi runtime.
|
|
93
137
|
*/
|
|
94
138
|
function listTheme(theme: PanelTheme): SettingsListTheme {
|
|
95
139
|
return {
|
|
@@ -110,16 +154,17 @@ function displayPath(file: string): string {
|
|
|
110
154
|
function item(spec: SettingSpec): SettingItem {
|
|
111
155
|
const value = loadConfig()[spec.field] ? spec.on : spec.off;
|
|
112
156
|
const override = envOverride(spec.field);
|
|
157
|
+
const label = spec.label.padEnd(LABEL_WIDTH);
|
|
113
158
|
if (override) {
|
|
114
159
|
// No values: Enter/Space leaves it alone, and the description says why.
|
|
115
160
|
return {
|
|
116
161
|
id: spec.field,
|
|
117
|
-
label
|
|
162
|
+
label,
|
|
118
163
|
currentValue: value,
|
|
119
164
|
description: `${spec.description}\n${override} overrides this while set; unset it to change it here.`,
|
|
120
165
|
};
|
|
121
166
|
}
|
|
122
|
-
return { id: spec.field, label
|
|
167
|
+
return { id: spec.field, label, currentValue: value, values: [spec.on, spec.off], description: spec.description };
|
|
123
168
|
}
|
|
124
169
|
|
|
125
170
|
export interface SettingsPanelOptions {
|
|
@@ -137,36 +182,69 @@ export interface SettingsPanel {
|
|
|
137
182
|
handleInput(data: string): void;
|
|
138
183
|
}
|
|
139
184
|
|
|
185
|
+
const HINT = " Enter/Space to change \u00b7 Tab/Shift+Tab to switch pages \u00b7 Esc to close";
|
|
186
|
+
|
|
187
|
+
function isNextPageKey(data: string): boolean {
|
|
188
|
+
return data === "\t" || matchesKey(data, "tab");
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
function isPreviousPageKey(data: string): boolean {
|
|
192
|
+
// CSI Z is how most terminals send Shift+Tab.
|
|
193
|
+
return data === "\x1b[Z" || matchesKey(data, "shift+tab");
|
|
194
|
+
}
|
|
195
|
+
|
|
140
196
|
export function settingsPanel(options: SettingsPanelOptions): SettingsPanel {
|
|
141
197
|
const { theme } = options;
|
|
142
|
-
const
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
198
|
+
const lists = PAGES.map((page) => {
|
|
199
|
+
const list: SettingsList = new SettingsList(
|
|
200
|
+
page.settings.map(item),
|
|
201
|
+
page.settings.length,
|
|
202
|
+
listTheme(theme),
|
|
203
|
+
(id, value) => {
|
|
204
|
+
const spec = page.settings.find((candidate) => candidate.field === id);
|
|
205
|
+
if (!spec) return;
|
|
206
|
+
options.onChange(spec.field, value === spec.on);
|
|
207
|
+
// Show what is in effect now.
|
|
208
|
+
list.updateValue(id, loadConfig()[spec.field] ? spec.on : spec.off);
|
|
209
|
+
},
|
|
210
|
+
() => options.onClose(),
|
|
211
|
+
);
|
|
212
|
+
return list;
|
|
213
|
+
});
|
|
214
|
+
let active = 0;
|
|
155
215
|
|
|
156
|
-
const
|
|
157
|
-
const
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
216
|
+
const rule = (width: number) => theme.fg("dim", "\u2500".repeat(Math.max(0, width)));
|
|
217
|
+
const tabBar = (width: number) => {
|
|
218
|
+
const tabs = PAGES.map((page, index) =>
|
|
219
|
+
index === active ? theme.fg("text", theme.bold(page.label)) : theme.fg("dim", page.label),
|
|
220
|
+
).join(theme.fg("dim", " / "));
|
|
221
|
+
// The file the settings are saved in, on the right when it fits.
|
|
222
|
+
const file = displayPath(options.file);
|
|
223
|
+
const gap = width - visibleWidth(tabs) - visibleWidth(file);
|
|
224
|
+
return gap >= 2 ? `${tabs}${" ".repeat(gap)}${theme.fg("dim", file)}` : truncateToWidth(tabs, width);
|
|
225
|
+
};
|
|
226
|
+
/** The open page's list, without SettingsList's own hint: the panel has one below. */
|
|
227
|
+
const body = (width: number) => {
|
|
228
|
+
const lines = lists[active].render(width);
|
|
229
|
+
const hint = lines.findIndex((line) => line.includes("Esc to cancel"));
|
|
230
|
+
const kept = hint === -1 ? lines : lines.slice(0, hint);
|
|
231
|
+
while (kept.length > 0 && kept[kept.length - 1].trim() === "") kept.pop();
|
|
232
|
+
return kept;
|
|
233
|
+
};
|
|
165
234
|
|
|
166
235
|
return {
|
|
167
|
-
render: (width) =>
|
|
168
|
-
|
|
169
|
-
|
|
236
|
+
render: (width) => {
|
|
237
|
+
const safe = Math.max(0, Math.floor(width));
|
|
238
|
+
return [rule(safe), tabBar(safe), rule(safe), ...body(safe), rule(safe), truncateToWidth(theme.fg("dim", HINT), safe), rule(safe)];
|
|
239
|
+
},
|
|
240
|
+
invalidate: () => {
|
|
241
|
+
for (const list of lists) list.invalidate();
|
|
242
|
+
},
|
|
243
|
+
handleInput: (data) => {
|
|
244
|
+
if (isNextPageKey(data)) active = (active + 1) % PAGES.length;
|
|
245
|
+
else if (isPreviousPageKey(data)) active = (active - 1 + PAGES.length) % PAGES.length;
|
|
246
|
+
else lists[active].handleInput(data);
|
|
247
|
+
},
|
|
170
248
|
};
|
|
171
249
|
}
|
|
172
250
|
|
|
@@ -188,15 +266,22 @@ function showSettings(ctx: ExtensionContext): void {
|
|
|
188
266
|
const config = loadConfig();
|
|
189
267
|
const lines = [
|
|
190
268
|
`pi-cc settings \u00b7 ${configPath()}`,
|
|
269
|
+
PAGES[0].label,
|
|
191
270
|
` messages: ${config.compactMessages ? "compact" : "card"}${overrideNote("compactMessages")}`,
|
|
192
271
|
` footer: ${config.footer ? "summary" : "hidden"}${overrideNote("footer")}`,
|
|
272
|
+
PAGES[1].label,
|
|
193
273
|
` draft: ${config.draft ? "on" : "off"}${overrideNote("draft")}`,
|
|
194
274
|
` summary: ${config.turnSummary ? "on" : "off"}${overrideNote("turnSummary")}`,
|
|
275
|
+
PAGES[2].label,
|
|
276
|
+
` compact: ${config.compaction ? "on" : "off"}${overrideNote("compaction")}`,
|
|
277
|
+
` idle: ${config.idleCompaction ? "on" : "off"}${overrideNote("idleCompaction")}`,
|
|
195
278
|
"",
|
|
196
279
|
"Usage: /pi_cc messages on|off",
|
|
197
280
|
" /pi_cc footer on|off",
|
|
198
281
|
" /pi_cc draft on|off",
|
|
199
282
|
" /pi_cc summary on|off",
|
|
283
|
+
" /pi_cc compact on|off",
|
|
284
|
+
" /pi_cc idle on|off",
|
|
200
285
|
];
|
|
201
286
|
notify(ctx, lines.join("\n"), "info");
|
|
202
287
|
}
|
|
@@ -242,6 +327,9 @@ const KEYS: Record<string, { field: keyof PiCcConfig; label: string }> = {
|
|
|
242
327
|
draft: { field: "draft", label: "Draft recall" },
|
|
243
328
|
summary: { field: "turnSummary", label: "Turn summary" },
|
|
244
329
|
turnsummary: { field: "turnSummary", label: "Turn summary" },
|
|
330
|
+
compact: { field: "compaction", label: "CC compaction" },
|
|
331
|
+
compaction: { field: "compaction", label: "CC compaction" },
|
|
332
|
+
idle: { field: "idleCompaction", label: "Idle compaction" },
|
|
245
333
|
};
|
|
246
334
|
|
|
247
335
|
function setSetting(ctx: ExtensionContext, apply: ApplySetting, key: string, raw: string | undefined): void {
|
|
@@ -293,7 +381,7 @@ export function registerPiCcCommand(pi: ExtensionAPI, owner: string): void {
|
|
|
293
381
|
};
|
|
294
382
|
|
|
295
383
|
pi.registerCommand("pi_cc", {
|
|
296
|
-
description: "pi-cc settings: message style, footer summary",
|
|
384
|
+
description: "pi-cc settings: message style, footer, draft, turn summary, compaction",
|
|
297
385
|
handler: async (args: string, ctx: ExtensionCommandContext) => {
|
|
298
386
|
const words = args.trim().split(/\s+/).filter(Boolean);
|
|
299
387
|
// "/pi_cc settings footer on" reads like the old /loop settings: drop the noun.
|