@foolsecret/pi-prompt 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/src/time.ts ADDED
@@ -0,0 +1,40 @@
1
+ /**
2
+ * 北京时间工具(低层纯函数,无项目内依赖)。
3
+ *
4
+ * 为什么单独一层:`isDeepSeekPeakHours` 被 auto(选档)与 stats(兜底价格)
5
+ * 双方使用;若留在 auto.ts,stats.ts import 它就会与 auto.ts→stats.ts
6
+ * 的导入形成循环(参见 pi-pricer 踩坑 #2:共享函数必须下沉到低层模块)。
7
+ */
8
+
9
+ /** 北京时间偏移(UTC+8,无夏令时) */
10
+ const BEIJING_OFFSET_MS = 8 * 60 * 60 * 1000;
11
+
12
+ /**
13
+ * DeepSeek 高峰时段(北京时间):工作日 9-12 与 14-18;周六/周日全天非高峰。
14
+ * 注意:这是**硬编码的公开时段**,仅用于内置兜底价格;用户装了 pi-pricer 后,
15
+ * 峰谷由其在 model-pricing.json 里配置的 schedule 决定。
16
+ */
17
+ export function isDeepSeekPeakHours(date: Date): boolean {
18
+ // +8h 后的 UTC 周几/几点 = 北京周几/几点
19
+ const bj = new Date(date.getTime() + BEIJING_OFFSET_MS);
20
+ const bjDay = bj.getUTCDay();
21
+ if (bjDay === 0 || bjDay === 6) return false; // 周日/周六全天非峰
22
+ const bjHour = bj.getUTCHours();
23
+ return (bjHour >= 9 && bjHour < 12) || (bjHour >= 14 && bjHour < 18);
24
+ }
25
+
26
+ /** 北京时区(UTC+8)的 YYYY-MM-DD 字符串(抽样日计数/频控用) */
27
+ export function beijingDate(date: Date): string {
28
+ const bj = new Date(date.getTime() + BEIJING_OFFSET_MS);
29
+ const y = bj.getUTCFullYear();
30
+ const m = String(bj.getUTCMonth() + 1).padStart(2, "0");
31
+ const d = String(bj.getUTCDate()).padStart(2, "0");
32
+ return `${y}-${m}-${d}`;
33
+ }
34
+
35
+ /** 时间戳归一为 Date(缺省 = 此刻);数值视为毫秒时间戳 */
36
+ export function toDate(timestamp?: Date | number): Date {
37
+ if (timestamp instanceof Date) return timestamp;
38
+ if (typeof timestamp === "number") return new Date(timestamp);
39
+ return new Date();
40
+ }
package/src/ui.ts ADDED
@@ -0,0 +1,156 @@
1
+ /**
2
+ * 三轴设置抽屉(复刻 pi /settings 的交互效果):上下 DynamicBorder 彩色边框 +
3
+ * 中间 SettingsList,选中行时说明栏随键盘上下切换(description 即"帮助栏/说明栏")。
4
+ *
5
+ * 设计规范见仓库根 DESIGN.md(design-md 格式):本模块是可复用部件,
6
+ * 只负责"渲染 + 把变更转发给回调",不接触配置/台账(由调用方接线)。
7
+ */
8
+
9
+ import { DynamicBorder, getSettingsListTheme } from "@earendil-works/pi-coding-agent";
10
+ import { Container, SettingsList, Text, type SettingItem } from "@earendil-works/pi-tui";
11
+ import type { DoMode, ShowMode, WriteMode } from "./modes.ts";
12
+ import type { AxisName } from "./command.ts";
13
+
14
+ /** 抽屉展示所需的当前状态:会话轴 + 默认轴(各三档) */
15
+ export interface PromptConfigDrawerState {
16
+ session: { show: ShowMode; write: WriteMode; do: DoMode };
17
+ defaults: { show: ShowMode; write: WriteMode; do: DoMode };
18
+ }
19
+
20
+ /** 抽屉各行的值域(show 含 auto 元档位,放第一项便于循环命中"自动") */
21
+ const SHOW_VALUES: readonly string[] = ["auto", "normal", "lite", "full", "ultra"];
22
+ const WRITE_VALUES: readonly string[] = ["minimal", "normal", "complete"];
23
+ const DO_VALUES: readonly string[] = ["direct", "normal", "deep"];
24
+
25
+ /** 设置项 id 的取值:会话轴 show|write|do;默认档 default-<轴> */
26
+ const SESSION_IDS: readonly AxisName[] = ["show", "write", "do"];
27
+ const DEFAULT_PREFIX = "default-";
28
+
29
+ /**
30
+ * 依据当前状态生成设置项列表(纯函数,不依赖 TUI,可单测)。
31
+ * 每项都有 description —— SettingsList 会把选中项的 description 渲染在
32
+ * 提示行上方,这就是"说明栏随光标切换"的机制。
33
+ */
34
+ export function buildSettingItems(state: PromptConfigDrawerState): SettingItem[] {
35
+ const session: SettingItem[] = [
36
+ {
37
+ id: "show",
38
+ label: "会话输出风格(show)",
39
+ currentValue: state.session.show,
40
+ values: [...SHOW_VALUES],
41
+ description:
42
+ "输出风格:auto 按成本函数每轮自动选档;ultra 电报式最省、full 常规精炼、lite 轻柔、normal 不注入(基础行为)。",
43
+ },
44
+ {
45
+ id: "write",
46
+ label: "会话代码量(write)",
47
+ currentValue: state.session.write,
48
+ values: [...WRITE_VALUES],
49
+ description: "代码写量:minimal 只交付最小可跑实现、normal 常规、complete 富注释带文档。",
50
+ },
51
+ {
52
+ id: "do",
53
+ label: "会话行为力度(do)",
54
+ currentValue: state.session.do,
55
+ values: [...DO_VALUES],
56
+ description: "行为力度:direct 收敛最小步骤、normal 常规、deep 主动查证根因与边界。",
57
+ },
58
+ ];
59
+ const defaults: SettingItem[] = [
60
+ {
61
+ id: `${DEFAULT_PREFIX}show`,
62
+ label: "默认输出风格(show)",
63
+ currentValue: state.defaults.show,
64
+ values: [...SHOW_VALUES],
65
+ description: "新会话默认的输出风格(后续会话生效,不影响当前会话)。",
66
+ },
67
+ {
68
+ id: `${DEFAULT_PREFIX}write`,
69
+ label: "默认代码量(write)",
70
+ currentValue: state.defaults.write,
71
+ values: [...WRITE_VALUES],
72
+ description: "新会话默认的代码写量。",
73
+ },
74
+ {
75
+ id: `${DEFAULT_PREFIX}do`,
76
+ label: "默认行为力度(do)",
77
+ currentValue: state.defaults.do,
78
+ values: [...DO_VALUES],
79
+ description: "新会话默认的行为力度。",
80
+ },
81
+ ];
82
+ return [...session, ...defaults];
83
+ }
84
+
85
+ /** 设置项 id → 是否为默认档行,以及对应的轴 */
86
+ export function resolveSettingId(id: string): { axis: AxisName; isDefault: boolean } | null {
87
+ if (SESSION_IDS.includes(id as AxisName)) return { axis: id as AxisName, isDefault: false };
88
+ if (id.startsWith(DEFAULT_PREFIX)) {
89
+ const axis = id.slice(DEFAULT_PREFIX.length) as AxisName;
90
+ if (SESSION_IDS.includes(axis)) return { axis, isDefault: true };
91
+ }
92
+ return null;
93
+ }
94
+
95
+ /** 抽屉所需的回调接线(由 PromptExtension 提供,注入以便单测) */
96
+ export interface PromptConfigDrawerCallbacks {
97
+ onSessionAxisChange: (axis: AxisName, value: string) => void;
98
+ onDefaultAxisChange: (axis: AxisName, value: string) => void;
99
+ }
100
+
101
+ /**
102
+ * 三轴设置抽屉:open 时在 ctx.ui 上开一个 /settings 同款抽屉;
103
+ * 无 TUI(非交互模式)时交给 onUnavailable 回退。
104
+ */
105
+ export class PromptConfigDrawer {
106
+ constructor(
107
+ private readonly readState: () => PromptConfigDrawerState,
108
+ private readonly callbacks: PromptConfigDrawerCallbacks,
109
+ private readonly onUnavailable: (context: unknown) => void,
110
+ ) {}
111
+
112
+ /** 打开抽屉;无 custom UI 时走回退分支 */
113
+ async open(context: unknown): Promise<void> {
114
+ const ui = (context as { ui?: { custom?: unknown } })?.ui;
115
+ if (typeof ui?.custom !== "function") {
116
+ this.onUnavailable(context);
117
+ return;
118
+ }
119
+ const items = buildSettingItems(this.readState());
120
+ await (ui.custom as (factory: unknown) => Promise<unknown>)(
121
+ (tui: unknown, theme: { fg: (color: string, text: string) => string; bold: (text: string) => string }, _kb: unknown, done: (result?: undefined) => void) => {
122
+ const container = new Container();
123
+ container.addChild(new DynamicBorder((s: string) => theme.fg("borderAccent", s)));
124
+ container.addChild(
125
+ new Text(theme.fg("accent", theme.bold("pi-prompt 设置")), 1, 1),
126
+ );
127
+ const settingsList = new SettingsList(
128
+ items,
129
+ Math.min(items.length + 4, 12),
130
+ getSettingsListTheme(),
131
+ (id, newValue) => this.handleChange(String(id), String(newValue)),
132
+ () => done(undefined),
133
+ { enableSearch: true },
134
+ );
135
+ container.addChild(settingsList);
136
+ container.addChild(new DynamicBorder((s: string) => theme.fg("borderAccent", s)));
137
+ return {
138
+ render: (w: number) => container.render(w),
139
+ invalidate: () => container.invalidate(),
140
+ handleInput: (data: unknown) => settingsList.handleInput?.(data as string),
141
+ };
142
+ },
143
+ );
144
+ }
145
+
146
+ /** 行变更分发:会话行 vs 默认档行,分别走对应回调 */
147
+ private handleChange(id: string, value: string): void {
148
+ const resolved = resolveSettingId(id);
149
+ if (!resolved) return;
150
+ if (resolved.isDefault) {
151
+ this.callbacks.onDefaultAxisChange(resolved.axis, value);
152
+ } else {
153
+ this.callbacks.onSessionAxisChange(resolved.axis, value);
154
+ }
155
+ }
156
+ }