@sendevia/pi-ask-author 1.0.1 → 1.0.3
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 -3
- package/dist/component.d.ts +322 -0
- package/{src/format.ts → dist/format.d.ts} +12 -118
- package/dist/index.d.ts +51 -0
- package/dist/index.js +1 -0
- package/dist/model.d.ts +260 -0
- package/dist/novel-markdown.d.ts +28 -0
- package/dist/sanitize.d.ts +18 -0
- package/dist/schema.d.ts +129 -0
- package/dist/texts.d.ts +494 -0
- package/{src/theme.ts → dist/theme.d.ts} +0 -2
- package/dist/view-rows.d.ts +69 -0
- package/dist/view-summary.d.ts +27 -0
- package/package.json +10 -10
- package/src/component.ts +0 -1219
- package/src/index.ts +0 -419
- package/src/model.ts +0 -748
- package/src/novel-markdown.ts +0 -673
- package/src/sanitize.ts +0 -279
- package/src/schema.ts +0 -108
- package/src/texts.ts +0 -636
- package/src/view-rows.ts +0 -217
- package/src/view-summary.ts +0 -110
- package/tsconfig.json +0 -16
package/README.md
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
# pi-ask-author
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
[](https://www.typescriptlang.org/)
|
|
5
|
-
[](https://opensource.org/licenses/MIT)
|
|
3
|
+

|
|
4
|
+
[](https://www.typescriptlang.org/)
|
|
5
|
+
[](https://opensource.org/licenses/MIT)
|
|
6
6
|
|
|
7
7
|
专为作家、轻小说创作者与 AI 协同写作设计的 [Pi](https://github.com/earendil-works/pi-coding-agent) 终端交互式请示扩展。
|
|
8
8
|
|
|
@@ -0,0 +1,322 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file component.ts
|
|
3
|
+
* @description `ask_author` 交互式 TUI 主组件(双栏问卷视图与答卷总览)
|
|
4
|
+
*
|
|
5
|
+
* 核心架构特性:
|
|
6
|
+
* - 宽屏双栏 / 窄屏单栏自适应:`renderWidth >= LAYOUT_CONFIG.splitViewMinWidth` 且题目含富内容时走双栏,否则降级单栏紧凑列表;预览与总览视口行数随终端高度收缩
|
|
7
|
+
* - 多级缓存失效:整屏(尺寸键 + 状态突变清空)、总览 / Tab 栏 / 左栏 / 菜单项(`revision` 键)、预览池(内容键);视口切片 O(1);`render()` 无 I/O,Markdown 渲染仅发生在派生缓存未命中时
|
|
8
|
+
* - 逐题独立光标记忆与 `1~9` 跳转题目;内置编辑器透传 Focusable 以定位 IME 候选框
|
|
9
|
+
* - Esc / Ctrl+C 双通道取消(输入模式二次确认);定时器、Abort 监听与缓存池全量回收
|
|
10
|
+
*
|
|
11
|
+
* 渲染纯度不变量:`render()` 及其可达被调函数禁止写入语义状态(`currentTab`、光标与各 `*ScrollOffset`、`inputMode`、`editTarget`、`editorEscArmed`、`statusMessage`、`isFinished`);滚动位移归一化一律归属输入路径
|
|
12
|
+
* 可写入透明备忘:`cached*`、`previewCache`、`tabCache`、`leftColCache`、`summaryCachedLines`、`menuCache`、`previewViewportObservation` 均为「渲染输入的纯函数」缓存
|
|
13
|
+
*
|
|
14
|
+
* 依赖方向:component.ts → theme / texts / format / model / novel-markdown / view-rows / view-summary;本组件只做按键路由、视口切片与整屏缓存编排
|
|
15
|
+
*/
|
|
16
|
+
import { type Component, type Focusable, type TUI } from "@earendil-works/pi-tui";
|
|
17
|
+
import { AskAuthorFormModel, type AskAuthorResult } from "./model.js";
|
|
18
|
+
import type { Theme } from "./theme.js";
|
|
19
|
+
/**
|
|
20
|
+
* `ask_author` 交互式 TUI 组件(问卷作答与答卷总览)
|
|
21
|
+
* 状态机:作答突变经 `AskAuthorFormModel`,`revision` 驱动两级缓存失效;`render()` 只写入透明备忘 / 观察缓存,绝不写回滚动位移等语义状态;`dispose()` 幂等且只设置 `disposed`,`finish()` 以 `isFinished` 保证 `done` 至多投递一次
|
|
22
|
+
*/
|
|
23
|
+
export declare class AskAuthorComponent implements Component, Focusable {
|
|
24
|
+
/** Focusable 接口要求:输入模式焦点指示 */
|
|
25
|
+
private _focused;
|
|
26
|
+
/** 当前焦点状态(仅 `true` 时透传内置 Editor 以定位 IME 候选框) */
|
|
27
|
+
get focused(): boolean;
|
|
28
|
+
/**
|
|
29
|
+
* 设置焦点状态并级联透传至内置 Editor(同值为无操作)
|
|
30
|
+
* @param value - 目标焦点状态;同时失效整屏 / Tab 栏 / 左栏缓存
|
|
31
|
+
*/
|
|
32
|
+
set focused(value: boolean);
|
|
33
|
+
/** TUI 上下文 */
|
|
34
|
+
private readonly tui;
|
|
35
|
+
/** 主题 */
|
|
36
|
+
private readonly theme;
|
|
37
|
+
/** 问卷模型(作答状态唯一权威,mutation 自动推进 revision) */
|
|
38
|
+
private readonly model;
|
|
39
|
+
/** 结果达成回调 */
|
|
40
|
+
private readonly done;
|
|
41
|
+
/** 当前 Tab 索引(0-based;questionCount 为总览确认页) */
|
|
42
|
+
private currentTab;
|
|
43
|
+
/** 每道题独立的光标索引记忆表 */
|
|
44
|
+
private readonly cursorByTab;
|
|
45
|
+
/** 当前题目菜单内选中的条目索引 */
|
|
46
|
+
private currentOptionIndex;
|
|
47
|
+
/** 左侧菜单列表滚动位移(行) */
|
|
48
|
+
private listScrollOffset;
|
|
49
|
+
/** 详情预览区滚动位移(行) */
|
|
50
|
+
private previewScrollOffset;
|
|
51
|
+
/** 答卷总览页滚动位移(行) */
|
|
52
|
+
private summaryScrollOffset;
|
|
53
|
+
/** 是否处于补充说明文本输入模式 */
|
|
54
|
+
private inputMode;
|
|
55
|
+
/** Esc 丢弃确认武装标记:编辑器非空时首次 Esc 仅武装,再次 Esc 才丢弃内容 */
|
|
56
|
+
private editorEscArmed;
|
|
57
|
+
/** 底部临时状态通知文案 */
|
|
58
|
+
private statusMessage;
|
|
59
|
+
/** 状态通知定时器 */
|
|
60
|
+
private statusTimer;
|
|
61
|
+
/** 是否已终结(终结后忽略一切输入,且 `done` 不再投递) */
|
|
62
|
+
private isFinished;
|
|
63
|
+
/** 是否已卸载回收(幂等标记;卸载后忽略迟到输入且拒绝再绑定 Abort 监听;`done` 投递不受本标记阻断,见 {@link dispose}) */
|
|
64
|
+
private disposed;
|
|
65
|
+
/** AbortSignal 监听绑定对象 */
|
|
66
|
+
private abortBinding?;
|
|
67
|
+
/** 整屏缓存键:调用方真实列宽(输出截断依据) */
|
|
68
|
+
private cachedWidth?;
|
|
69
|
+
/** 整屏缓存键:布局列宽 `max(20, width)`(折行依据,20 列为窄终端宽度下限) */
|
|
70
|
+
private cachedLayoutWidth?;
|
|
71
|
+
/** 整屏缓存键:终端行数(视口行数随之自适应) */
|
|
72
|
+
private cachedHeight?;
|
|
73
|
+
/** 整屏已格式化终端行缓存 */
|
|
74
|
+
private cachedLines?;
|
|
75
|
+
/** 预览框完整渲染行缓存池(键:题号+条目身份+列宽+补充说明;超 {@link PREVIEW_CACHE_CAPACITY} 按插入序淘汰) */
|
|
76
|
+
private readonly previewCache;
|
|
77
|
+
/** 总览页未切片内容行缓存(键:列宽 + `revision`) */
|
|
78
|
+
private summaryCachedLines?;
|
|
79
|
+
/** Tab 栏行缓存(键:当前页 + `revision` + 列宽) */
|
|
80
|
+
private tabCache?;
|
|
81
|
+
/** 左栏菜单行缓存(键:题号 + 光标 + 位移 + `revision` + 左栏宽) */
|
|
82
|
+
private leftColCache?;
|
|
83
|
+
/** 菜单项派生缓存(键:题号 + `revision`,消除 render/输入路径重复构建) */
|
|
84
|
+
private menuCache?;
|
|
85
|
+
/** 上一帧预览视口的观察元数据(渲染写入、输入路径读取以归一化位移) */
|
|
86
|
+
private previewViewportObservation?;
|
|
87
|
+
/** 当前编辑器针对的目标(题目整体或指定选项) */
|
|
88
|
+
private editTarget?;
|
|
89
|
+
/** 内置补充说明编辑器实例 */
|
|
90
|
+
private readonly editor;
|
|
91
|
+
/**
|
|
92
|
+
* 构造交互式 TUI 主组件
|
|
93
|
+
* @param tui - TUI 上下文
|
|
94
|
+
* @param theme - 主题
|
|
95
|
+
* @param model - 问卷模型
|
|
96
|
+
* @param done - 结果回调
|
|
97
|
+
*/
|
|
98
|
+
constructor(tui: TUI, theme: Theme, model: AskAuthorFormModel, done: (result: AskAuthorResult) => void);
|
|
99
|
+
/**
|
|
100
|
+
* 绑定中止信号(幂等,至多绑定一次)
|
|
101
|
+
* @param signal - 外层 AbortSignal;缺省 / 已绑定 / 已卸载时直接返回
|
|
102
|
+
*/
|
|
103
|
+
bindAbortSignal(signal?: AbortSignal): void;
|
|
104
|
+
/**
|
|
105
|
+
* 清理定时器、Abort 监听与全部缓存池(幂等)
|
|
106
|
+
* 只设置 `disposed`、不设置 `isFinished`(资源回收 ≠ `done` 已投递),否则宿主先卸载而 `done` 未投递时挂起
|
|
107
|
+
*/
|
|
108
|
+
dispose(): void;
|
|
109
|
+
/** 清空整屏帧缓存(列宽 / 行数键;不触发重绘) */
|
|
110
|
+
private clearFrameCaches;
|
|
111
|
+
/** 清空全部派生缓存(`revision` 键与内容键:预览池 / 总览 / Tab 栏 / 左栏 / 菜单项;不触发重绘) */
|
|
112
|
+
private clearDerivedCaches;
|
|
113
|
+
/** 状态突变时失效整屏缓存并请求重绘(保留派生缓存池复用) */
|
|
114
|
+
private touch;
|
|
115
|
+
/** 作答变更后的统一失效入口:模型已推进 revision,此处清空全部派生缓存 */
|
|
116
|
+
private touchDerived;
|
|
117
|
+
/**
|
|
118
|
+
* Component 接口约定:宿主重绘上下文(主题切换 / 需从头重绘)时清空全部渲染缓存
|
|
119
|
+
* 终端尺寸变化不调用本方法(宿主 resize 仅 `requestRender`),由整屏缓存键绑定列宽与行数自然失效
|
|
120
|
+
*/
|
|
121
|
+
invalidate(): void;
|
|
122
|
+
/** 清理临时状态定时器(清空引用以便后续 setStatus 重新计时) */
|
|
123
|
+
private clearStatusTimer;
|
|
124
|
+
/**
|
|
125
|
+
* 触发底部临时状态通知
|
|
126
|
+
* @param msg - 提示文案(取自 `TEXTS.status`,不做控制字符清洗)
|
|
127
|
+
*/
|
|
128
|
+
private setStatus;
|
|
129
|
+
/**
|
|
130
|
+
* 终结作答流程并触发 `done`(至多投递一次)
|
|
131
|
+
* `isFinished` 单守卫防止重复投递;不以 `disposed` 阻断(资源已回收但 `done` 未投递时仍须结算),跳转入 {@link dispose}
|
|
132
|
+
* @param cancelled - 是否用户主动取消
|
|
133
|
+
*/
|
|
134
|
+
finish(cancelled: boolean): void;
|
|
135
|
+
/** Tab 总页数(题目页 + 总览提交页) */
|
|
136
|
+
private get totalTabs();
|
|
137
|
+
/** 当前 Tab 是否为答卷总览提交页 */
|
|
138
|
+
private get isSubmitTab();
|
|
139
|
+
/** 终端可视行数(`tui.terminal.rows` 不可用或非正数时兜底 24) */
|
|
140
|
+
private get terminalRows();
|
|
141
|
+
/** 总览页视口行数上限(随终端高度自适应;区间 `[4, LAYOUT_CONFIG.summaryPageSize]`) */
|
|
142
|
+
private get summaryPageSize();
|
|
143
|
+
/** 双栏右侧预览视口行数上限(区间 `[6, LAYOUT_CONFIG.splitPreviewHeight]`) */
|
|
144
|
+
private get splitPreviewHeight();
|
|
145
|
+
/** 单栏内联预览视口行数上限(区间 `[3, LAYOUT_CONFIG.singlePreviewHeight]`) */
|
|
146
|
+
private get singlePreviewHeight();
|
|
147
|
+
/**
|
|
148
|
+
* 当前题目的菜单项
|
|
149
|
+
* @returns 菜单项数组;按 `(题号, revision)` 记忆化,命中时零分配复用
|
|
150
|
+
*/
|
|
151
|
+
private getMenuItems;
|
|
152
|
+
/**
|
|
153
|
+
* 当前聚焦菜单项
|
|
154
|
+
* @returns 越界安全查询结果;无菜单项或索引越界时为 undefined
|
|
155
|
+
*/
|
|
156
|
+
private currentMenuItem;
|
|
157
|
+
/**
|
|
158
|
+
* 切换题目 / 总览页 Tab,重置滚动位移并按 `cursorByTab` 恢复光标记忆
|
|
159
|
+
* @param newTab - 目标 Tab 索引(0-based;questionCount 为总览页;同页为无操作)
|
|
160
|
+
*/
|
|
161
|
+
private switchTab;
|
|
162
|
+
/** 收敛光标至合法区间并同步左栏滚动窗口,保证聚焦项可见(菜单项为空时直接返回) */
|
|
163
|
+
private adjustScrollForCurrentOption;
|
|
164
|
+
/**
|
|
165
|
+
* 移动菜单光标并同步滚动窗口,重置预览位移
|
|
166
|
+
* @param target - 绝对索引或 `{ delta }` 相对位移(越界由 clamp 收敛,菜单为空时无操作)
|
|
167
|
+
*/
|
|
168
|
+
private moveCursor;
|
|
169
|
+
/**
|
|
170
|
+
* 当前光标默认的编辑目标
|
|
171
|
+
* @returns 聚焦预设选项时为该选项,否则为题目整体
|
|
172
|
+
*/
|
|
173
|
+
private getEditTargetForCurrentSelection;
|
|
174
|
+
/**
|
|
175
|
+
* 打开补充说明编辑器(选项专属或题目整体)
|
|
176
|
+
* @param target - 编辑目标;缺省时按当前聚焦项推导(题目缺失或禁用补充说明时无操作)
|
|
177
|
+
*/
|
|
178
|
+
private openCustomEditor;
|
|
179
|
+
/** 关闭编辑器并还原非输入模式状态(清空缓冲与焦点;幂等) */
|
|
180
|
+
private closeEditor;
|
|
181
|
+
/**
|
|
182
|
+
* 确认当前题并跳转下一题 / 总览页(聚焦预设选项时先经模型写入选中)
|
|
183
|
+
* @param currentItem - 聚焦菜单项
|
|
184
|
+
* @param q - 当前题目(未达提交下限时仅提示状态栏,不切页)
|
|
185
|
+
*/
|
|
186
|
+
private confirmAndAdvance;
|
|
187
|
+
/**
|
|
188
|
+
* Component 接口约定:键盘事件三级路由(输入模式 → 全局键 → 分视图)
|
|
189
|
+
* Esc 与 Ctrl+C 同为取消键(宿主不会自行中止工具信号):常规模式直接 `finish(true)`,输入模式下编辑器非空时首次仅武装丢弃确认
|
|
190
|
+
* @param data - 终端按键序列
|
|
191
|
+
*/
|
|
192
|
+
handleInput(data: string): void;
|
|
193
|
+
/**
|
|
194
|
+
* 题目作答页常规键路由(移动 / 勾选 / 前进 / 补充说明;子路由均为先匹配先消费)
|
|
195
|
+
* @param data - 终端按键序列
|
|
196
|
+
*/
|
|
197
|
+
private handleQuestionInput;
|
|
198
|
+
/**
|
|
199
|
+
* 补充说明快捷键路由(`n`/`e` 补充当前聚焦项、`N` 题目整体补充、`x`/Delete 清空)
|
|
200
|
+
* @param q - 当前题目
|
|
201
|
+
* @param data - 按键序列
|
|
202
|
+
* @returns 是否已消费该按键
|
|
203
|
+
*/
|
|
204
|
+
private handleNoteShortcut;
|
|
205
|
+
/**
|
|
206
|
+
* 预览区垂直滚动键(PgUp / PgDn / 方括号 / Alt+上下)
|
|
207
|
+
* 滚动位移归一化归属输入路径:以渲染帧写入的 {@link previewViewportObservation} 为界经 `clampWindowOffset` 收敛(缺观察元数据时按单栏预览高度退化)
|
|
208
|
+
* @param data - 按键序列
|
|
209
|
+
* @returns 是否已消费该按键
|
|
210
|
+
*/
|
|
211
|
+
private handlePreviewScroll;
|
|
212
|
+
/**
|
|
213
|
+
* 选项菜单光标移动键(↑↓ / jk / Home / End)
|
|
214
|
+
* @param data - 按键序列
|
|
215
|
+
* @returns 是否已消费该按键
|
|
216
|
+
*/
|
|
217
|
+
private handleCursorNavigation;
|
|
218
|
+
/**
|
|
219
|
+
* 多选模式下 `a` 键全选 / 清空已选项
|
|
220
|
+
* @param q - 当前题目
|
|
221
|
+
* @param data - 按键序列
|
|
222
|
+
* @returns 是否已消费该按键
|
|
223
|
+
*/
|
|
224
|
+
private handleSelectAllShortcut;
|
|
225
|
+
/**
|
|
226
|
+
* 触发聚焦条目主动作(唯一入口为空格键:选项勾选 / 打开补充说明 / 推进确认)
|
|
227
|
+
* @param currentItem - 聚焦菜单项
|
|
228
|
+
* @param q - 当前题目(选项命中上限时仅提示状态栏)
|
|
229
|
+
*/
|
|
230
|
+
private activateMenuItem;
|
|
231
|
+
/**
|
|
232
|
+
* 总览页键路由(滚动与回车提交;`Ctrl+S` 同义提交)
|
|
233
|
+
* 空格更改为向下滚动(防止习惯性翻页误提交);位移归一化归属本路径,上界获取自 `summaryCachedLines` 总行数与 `summaryPageSize`,`End` 仅设置超大值,由后续输入收敛
|
|
234
|
+
* @param data - 按键序列
|
|
235
|
+
*/
|
|
236
|
+
private handleSummaryInput;
|
|
237
|
+
/**
|
|
238
|
+
* 计算总览页滚动步长
|
|
239
|
+
* @param data - 按键序列
|
|
240
|
+
* @returns 行位移(正下负上,0 为无动作)
|
|
241
|
+
*/
|
|
242
|
+
private summaryScrollStep;
|
|
243
|
+
/**
|
|
244
|
+
* Component 接口约定:渲染整屏行(带整屏缓存)
|
|
245
|
+
* 缓存键绑定真实列宽、布局列宽与终端行数(窄于 20 列时二者截断范围不同,仅绑定 layoutWidth 会串帧;行数变化必须整体重新计算);本方法及可达被调函数绝不写入语义状态,仅刷新透明备忘 / 观察缓存
|
|
246
|
+
* @param width - 终端真实可视宽度(布局宽度下限 20 列)
|
|
247
|
+
* @returns 不超过 width 的终端行数组(命中缓存时直接返回)
|
|
248
|
+
*/
|
|
249
|
+
render(width: number): string[];
|
|
250
|
+
/**
|
|
251
|
+
* 原地追加底部状态通知行(无文案时不追加,仅包含通知行与前面的空行)
|
|
252
|
+
* @param lines - 输出行数组(原地追加)
|
|
253
|
+
*/
|
|
254
|
+
private pushStatusMessage;
|
|
255
|
+
/**
|
|
256
|
+
* 渲染答卷总览与确认提交页(完成状态 / 已选项 / 补充说明 / 提交指引)
|
|
257
|
+
* 内容行由 `view-summary.ts` 构建并按 `(列宽, revision)` 缓存于 `summaryCachedLines`,本方法只做 O(1) 切片;位移在本地收敛,绝不写回 `summaryScrollOffset`(写入只发生在输入路径)
|
|
258
|
+
* @param lines - 输出行数组
|
|
259
|
+
* @param renderWidth - 终端布局列宽
|
|
260
|
+
*/
|
|
261
|
+
private renderSubmitSummary;
|
|
262
|
+
/**
|
|
263
|
+
* 渲染补充说明编辑器全屏模式(标题 / 上下文背景 / 编辑框)
|
|
264
|
+
* @param lines - 输出行数组
|
|
265
|
+
* @param renderWidth - 终端可视宽度
|
|
266
|
+
*/
|
|
267
|
+
private renderInputMode;
|
|
268
|
+
/**
|
|
269
|
+
* 渲染常规题目作答视图(自适应双栏分屏或单栏紧凑列表)
|
|
270
|
+
* @param lines - 输出行数组
|
|
271
|
+
* @param renderWidth - 终端可视宽度
|
|
272
|
+
*/
|
|
273
|
+
private renderQuestionView;
|
|
274
|
+
/**
|
|
275
|
+
* 获取聚焦条目的预渲染完整内容行(`previewCache` 透明备忘缓存,跨条目 / 切换题目零重复解析)
|
|
276
|
+
* @param item - 目标菜单项
|
|
277
|
+
* @param customText - 题目整体补充说明
|
|
278
|
+
* @param optionNote - 选项专属补充说明
|
|
279
|
+
* @param targetWidth - 预览可用列宽
|
|
280
|
+
* @returns 未切片的完整内容行数组;`item` 缺省时返回空数组
|
|
281
|
+
*/
|
|
282
|
+
private previewContentLines;
|
|
283
|
+
/**
|
|
284
|
+
* 计算并切片本帧预览框行(纯计算,不写回语义状态)
|
|
285
|
+
* 位移由 `previewScrollOffset` 与本帧总行数在本地收敛;本帧观察元数据写入 {@link previewViewportObservation} 供输入路径归一化位移
|
|
286
|
+
* @param item - 目标菜单项
|
|
287
|
+
* @param customText - 题目整体补充说明
|
|
288
|
+
* @param optionNote - 选项专属补充说明
|
|
289
|
+
* @param targetWidth - 预览可用列宽
|
|
290
|
+
* @param viewHeight - 视口最大行数
|
|
291
|
+
* @returns 本帧可视内容行(含底部滚动信息行)
|
|
292
|
+
*/
|
|
293
|
+
private buildPreviewBox;
|
|
294
|
+
/**
|
|
295
|
+
* 对预渲染完整内容行做 O(1) 滑动窗口切片并附加底部行号信息
|
|
296
|
+
* @param fullLines - 已折行完整内容行
|
|
297
|
+
* @param scrollOffset - 已由 {@link clampWindowOffset} 收敛的起始行位移
|
|
298
|
+
* @param viewHeight - 视口最大行数
|
|
299
|
+
* @param targetWidth - 预览可用列宽
|
|
300
|
+
* @returns 视口内容行与滚动信息行
|
|
301
|
+
*/
|
|
302
|
+
private slicePreviewBoxLines;
|
|
303
|
+
/**
|
|
304
|
+
* 渲染双栏视图(左选项菜单 + 右推演 / 分镜预览框)
|
|
305
|
+
* @param lines - 输出行数组
|
|
306
|
+
* @param q - 当前题目
|
|
307
|
+
* @param st - 作答状态
|
|
308
|
+
* @param items - 菜单项
|
|
309
|
+
* @param renderWidth - 终端可用宽度(不足侧补空行至两栏等高)
|
|
310
|
+
*/
|
|
311
|
+
private renderSplitView;
|
|
312
|
+
/**
|
|
313
|
+
* 遍历输出菜单项行,单栏模式下在聚焦项下方内联展开预览
|
|
314
|
+
* @param lines - 输出行数组
|
|
315
|
+
* @param items - 菜单项
|
|
316
|
+
* @param q - 当前题目
|
|
317
|
+
* @param st - 作答状态
|
|
318
|
+
* @param rowWidth - 选项行可用列宽
|
|
319
|
+
* @param expandFocused - 是否展开聚焦项预览(窗口外条目不访问)
|
|
320
|
+
*/
|
|
321
|
+
private pushListItems;
|
|
322
|
+
}
|
|
@@ -8,10 +8,6 @@
|
|
|
8
8
|
*
|
|
9
9
|
* 依赖方向:format.ts → texts.ts(布局常量与文案字典)
|
|
10
10
|
*/
|
|
11
|
-
|
|
12
|
-
import { truncateToWidth, visibleWidth, wrapTextWithAnsi } from "@earendil-works/pi-tui";
|
|
13
|
-
import { LAYOUT_CONFIG, TEXTS } from "./texts.js";
|
|
14
|
-
|
|
15
11
|
/**
|
|
16
12
|
* 将数值收敛至闭区间 `[min, max]`
|
|
17
13
|
* @param value - 待收敛数值
|
|
@@ -19,10 +15,7 @@ import { LAYOUT_CONFIG, TEXTS } from "./texts.js";
|
|
|
19
15
|
* @param max - 区间上界
|
|
20
16
|
* @returns 位于 `[min, max]` 区间内的数值;`min > max` 时恒返回 `min`
|
|
21
17
|
*/
|
|
22
|
-
export function clamp(value: number, min: number, max: number): number
|
|
23
|
-
return Math.max(min, Math.min(value, max));
|
|
24
|
-
}
|
|
25
|
-
|
|
18
|
+
export declare function clamp(value: number, min: number, max: number): number;
|
|
26
19
|
/**
|
|
27
20
|
* 将视口滚动位移收敛至合法区间,防止越界滚动造成空白视口
|
|
28
21
|
* @param offset - 待收敛的 0-based 起始行位移
|
|
@@ -30,10 +23,7 @@ export function clamp(value: number, min: number, max: number): number {
|
|
|
30
23
|
* @param viewHeight - 可视行数(调用方必须传入非负值)
|
|
31
24
|
* @returns `[0, max(0, total - viewHeight)]` 内的位移;列表为空或内容不足一屏(`total <= viewHeight`)时恒为 0
|
|
32
25
|
*/
|
|
33
|
-
export function clampWindowOffset(offset: number, total: number, viewHeight: number): number
|
|
34
|
-
return clamp(offset, 0, Math.max(0, total - viewHeight));
|
|
35
|
-
}
|
|
36
|
-
|
|
26
|
+
export declare function clampWindowOffset(offset: number, total: number, viewHeight: number): number;
|
|
37
27
|
/**
|
|
38
28
|
* 截取视口可视行切片(仅复制可视区,不遍历非可视区数据)
|
|
39
29
|
* @param lines - 完整内容行数组(只读,不修改入参)
|
|
@@ -41,33 +31,13 @@ export function clampWindowOffset(offset: number, total: number, viewHeight: num
|
|
|
41
31
|
* @param viewHeight - 可视行数
|
|
42
32
|
* @returns 至多 `viewHeight` 行的新数组;`lines` 为空、`viewHeight <= 0` 或 `offset` 越过末行时返回空数组
|
|
43
33
|
*/
|
|
44
|
-
export function windowSlice<T>(lines: readonly T[], offset: number, viewHeight: number): T[]
|
|
45
|
-
return lines.slice(offset, offset + viewHeight);
|
|
46
|
-
}
|
|
47
|
-
|
|
34
|
+
export declare function windowSlice<T>(lines: readonly T[], offset: number, viewHeight: number): T[];
|
|
48
35
|
/**
|
|
49
36
|
* 清除回车符并将制表符规整为双空格(纯文本规整,不剥离 ANSI 控制序列)
|
|
50
37
|
* @param str - 原始文本;缺省时按空串处理
|
|
51
38
|
* @returns 规整后的文本(入参为 undefined 时返回空串,永不返回 undefined)
|
|
52
39
|
*/
|
|
53
|
-
export function cleanText(str: string | undefined): string
|
|
54
|
-
return str ? str.replace(/\t/g, " ").replace(/\r/g, "") : "";
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
/**
|
|
58
|
-
* 剥离终端转义与控制字符(防止 ANSI 注入污染 TUI 渲染管线)
|
|
59
|
-
* 覆盖 OSC(BEL/ST 终止)、CSI、双字符转义与残余 C0 控制字符;换行符保留,由调用方决定单行化时机
|
|
60
|
-
* @param str - 已由 `cleanText` 规整的字符串(无回车与制表符)
|
|
61
|
-
* @returns 剥离全部转义序列与残余 C0 控制字符(含 DEL `\x7f`)的字符串;换行符 `\n` 保留
|
|
62
|
-
*/
|
|
63
|
-
function stripControlSequences(str: string): string {
|
|
64
|
-
return str
|
|
65
|
-
.replace(/\x1b\][^\x07\x1b]*(?:\x07|\x1b\\)/g, "")
|
|
66
|
-
.replace(/\x1b\[[0-?]*[ -/]*[@-~]/g, "")
|
|
67
|
-
.replace(/\x1b[@-_]/g, "")
|
|
68
|
-
.replace(/[\x00-\x08\x0b\x0c\x0e-\x1f\x7f]/g, "");
|
|
69
|
-
}
|
|
70
|
-
|
|
40
|
+
export declare function cleanText(str: string | undefined): string;
|
|
71
41
|
/**
|
|
72
42
|
* 裁剪空白并剥离终端控制序列;纯空白或剥离后为空的输入均返回 undefined
|
|
73
43
|
* 约定:返回值要么是非空字符串,要么是 undefined——调用方的 `?? 兜底` 不会漏接空串
|
|
@@ -75,49 +45,13 @@ function stripControlSequences(str: string): string {
|
|
|
75
45
|
* @param str - 原始不可信文本;缺省时返回 undefined
|
|
76
46
|
* @returns 清洗后的非空字符串;纯空白或剥离控制序列后为空时返回 undefined
|
|
77
47
|
*/
|
|
78
|
-
export function cleanOptional(str: string | undefined): string | undefined
|
|
79
|
-
const t = str?.trim();
|
|
80
|
-
return t ? stripControlSequences(cleanText(t)) || undefined : undefined;
|
|
81
|
-
}
|
|
82
|
-
|
|
48
|
+
export declare function cleanOptional(str: string | undefined): string | undefined;
|
|
83
49
|
/**
|
|
84
50
|
* 尝试 `JSON.parse`;非字符串原样返回,解析失败返回 undefined
|
|
85
51
|
* @param value - 待解析值
|
|
86
52
|
* @returns 解析结果或原值;字符串长度(UTF-16 码元)超过 1_000_000 时直接返回 undefined
|
|
87
53
|
*/
|
|
88
|
-
export function parseJson(value: unknown): unknown
|
|
89
|
-
if (typeof value === "string") {
|
|
90
|
-
if (value.length > 1_000_000) return undefined;
|
|
91
|
-
try {
|
|
92
|
-
return JSON.parse(value);
|
|
93
|
-
} catch {
|
|
94
|
-
return undefined;
|
|
95
|
-
}
|
|
96
|
-
}
|
|
97
|
-
return value;
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
/**
|
|
101
|
-
* 悬挂缩进折行:首行携带前缀,后续折行左缘与前缀可见宽度一致
|
|
102
|
-
* 宽度按 CJK 全角与 ANSI 转义精确计算(`visibleWidth` / `wrapTextWithAnsi`)
|
|
103
|
-
* @param prefix - 首行前缀(后续行以同宽空格悬挂缩进)
|
|
104
|
-
* @param text - 待折行文本
|
|
105
|
-
* @param width - 可视总宽度(列);调用方必须传入 `>= 1`,`width <= 0` 时不截断前缀
|
|
106
|
-
* @returns 折行后的行数组;`text` 无法产出任何行时返回 `[prefix]`(前缀非空)或空数组
|
|
107
|
-
*/
|
|
108
|
-
function hangText(prefix: string, text: string, width: number): string[] {
|
|
109
|
-
let prefixW = visibleWidth(prefix);
|
|
110
|
-
if (prefixW >= width && width > 0) {
|
|
111
|
-
prefix = truncateToWidth(prefix, width - 1);
|
|
112
|
-
prefixW = visibleWidth(prefix);
|
|
113
|
-
}
|
|
114
|
-
const availableW = Math.max(1, width - prefixW);
|
|
115
|
-
const pad = " ".repeat(prefixW);
|
|
116
|
-
const wrapped = wrapTextWithAnsi(text, availableW);
|
|
117
|
-
if (wrapped.length === 0) return prefix ? [prefix] : [];
|
|
118
|
-
return wrapped.map((line, i) => (i === 0 ? prefix + line : pad + line));
|
|
119
|
-
}
|
|
120
|
-
|
|
54
|
+
export declare function parseJson(value: unknown): unknown;
|
|
121
55
|
/**
|
|
122
56
|
* 将悬挂折行结果原地追加至目标行数组
|
|
123
57
|
* @param lines - 目标行数组(原地追加)
|
|
@@ -125,77 +59,37 @@ function hangText(prefix: string, text: string, width: number): string[] {
|
|
|
125
59
|
* @param width - 可视总宽度
|
|
126
60
|
* @param prefix - 首行前缀(后续折行等宽悬挂缩进)
|
|
127
61
|
*/
|
|
128
|
-
export function pushWrapped(lines: string[], text: string, width: number, prefix
|
|
129
|
-
lines.push(...hangText(prefix, text, width));
|
|
130
|
-
}
|
|
131
|
-
|
|
62
|
+
export declare function pushWrapped(lines: string[], text: string, width: number, prefix?: string): void;
|
|
132
63
|
/**
|
|
133
64
|
* 单行安全截断,保证可见宽度不超过 `maxWidth`(超宽部分截断,并沿用 `truncateToWidth` 的默认省略号 `...`)
|
|
134
65
|
* @param str - 原始文本(先经 `cleanText` 清除回车与制表符)
|
|
135
66
|
* @param maxWidth - 最大可见宽度(列);`<= 0` 时返回空串
|
|
136
67
|
* @returns 可见宽度不超过 `maxWidth` 的单行文本(未超宽时原样返回,不做补齐)
|
|
137
68
|
*/
|
|
138
|
-
export function safeLine(str: string, maxWidth: number): string
|
|
139
|
-
if (maxWidth <= 0) return "";
|
|
140
|
-
const cleaned = cleanText(str);
|
|
141
|
-
return visibleWidth(cleaned) > maxWidth ? truncateToWidth(cleaned, maxWidth) : cleaned;
|
|
142
|
-
}
|
|
143
|
-
|
|
69
|
+
export declare function safeLine(str: string, maxWidth: number): string;
|
|
144
70
|
/**
|
|
145
71
|
* 按可见宽度填充至指定宽度(支持 CJK 与 ANSI)
|
|
146
72
|
* @param str - 原始文本(先经 `cleanText` 清除回车与制表符)
|
|
147
73
|
* @param targetWidth - 目标可见宽度(列);`<= 0` 时返回空串
|
|
148
74
|
* @returns 可见宽度不超过 `targetWidth` 的单行文本:不足则右补空格,超宽则截断并以默认省略号 `...` 收尾(含全角字符时结果可能略窄于 `targetWidth`)
|
|
149
75
|
*/
|
|
150
|
-
export function padToVisibleWidth(str: string, targetWidth: number): string
|
|
151
|
-
if (targetWidth <= 0) return "";
|
|
152
|
-
const cleaned = cleanText(str);
|
|
153
|
-
const w = visibleWidth(cleaned);
|
|
154
|
-
if (w >= targetWidth) return truncateToWidth(cleaned, targetWidth);
|
|
155
|
-
return cleaned + " ".repeat(targetWidth - w);
|
|
156
|
-
}
|
|
157
|
-
|
|
76
|
+
export declare function padToVisibleWidth(str: string, targetWidth: number): string;
|
|
158
77
|
/**
|
|
159
78
|
* 生成指定宽度的横向分隔线
|
|
160
79
|
* @param width - 目标可见宽度(列)
|
|
161
80
|
* @param char - 填充字符(默认 `LAYOUT_CONFIG.dividerThin`,每字符宽度按 1 列计)
|
|
162
81
|
* @returns `width <= 0` 时返回空串,否则返回 `char` 重复 `width` 次
|
|
163
82
|
*/
|
|
164
|
-
export function divider(width: number, char
|
|
165
|
-
return width > 0 ? char.repeat(width) : "";
|
|
166
|
-
}
|
|
167
|
-
|
|
83
|
+
export declare function divider(width: number, char?: string): string;
|
|
168
84
|
/**
|
|
169
85
|
* 极限压缩正文草稿/分镜头为高密度单行 LLM 指令:逐行剥离行首 Markdown 标题标识与引用符,折叠连续空白,各片段以 `TEXTS.markdown.draftSegmentSeparator` 连接
|
|
170
86
|
* @param preview - 原始草稿/分镜头文本;缺省或全空白时返回 undefined
|
|
171
87
|
* @returns 单行分镜头指令;无有效片段时返回 undefined
|
|
172
88
|
*/
|
|
173
|
-
export function compressDraftPreview(preview: string | undefined): string | undefined
|
|
174
|
-
if (!preview) return undefined;
|
|
175
|
-
const cleaned = cleanText(preview);
|
|
176
|
-
if (!cleaned.trim()) return undefined;
|
|
177
|
-
|
|
178
|
-
const segments = cleaned
|
|
179
|
-
.split("\n")
|
|
180
|
-
.map((line) => {
|
|
181
|
-
let l = line.trim();
|
|
182
|
-
if (!l) return "";
|
|
183
|
-
// 单次交替剥离行首任意层级的 Markdown 标题标识与引用符(`> # 标题` 需同时去除两类前缀,故不可分成两次行首替换)
|
|
184
|
-
l = l.replace(/^(?:[>#]+\s*)+/, "");
|
|
185
|
-
l = l.replace(/\s+/g, " ");
|
|
186
|
-
return l;
|
|
187
|
-
})
|
|
188
|
-
.filter(Boolean);
|
|
189
|
-
|
|
190
|
-
if (segments.length === 0) return undefined;
|
|
191
|
-
return segments.join(TEXTS.markdown.draftSegmentSeparator);
|
|
192
|
-
}
|
|
193
|
-
|
|
89
|
+
export declare function compressDraftPreview(preview: string | undefined): string | undefined;
|
|
194
90
|
/**
|
|
195
91
|
* 折叠不可信文本的换行与连续空白为单空格:LLM 生成的描述/批注含换行会破坏返回封套的行结构
|
|
196
92
|
* @param text - 原始文本
|
|
197
93
|
* @returns 单行文本(所有 `\s+` 折叠为一个空格并去除首尾空白;纯空白输入返回空串)
|
|
198
94
|
*/
|
|
199
|
-
export function toSingleLine(text: string): string
|
|
200
|
-
return text.replace(/\s+/g, " ").trim();
|
|
201
|
-
}
|
|
95
|
+
export declare function toSingleLine(text: string): string;
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file index.ts
|
|
3
|
+
* @description 作者请示插件入口:注册 `ask_author` 工具与 `/ask-author` 命令
|
|
4
|
+
*
|
|
5
|
+
* 核心架构特性:
|
|
6
|
+
* - 四类出口(取消 / 结构化错误 / 运行期异常 / 正常完成)由四个结果工厂统一产出;其中取消、结构化错误与运行期异常三类共享 `ResultContext` 的缺省收敛,完成结果由表单模型直接供给
|
|
7
|
+
* - `prepareArguments` 防御性清洗兼容批量问卷与单题平铺双模式;返回文本恒以 `TEXTS.markdown` 的固定封套前缀开头(`[Author Decision Finalized]` / `[Author Decision Cancelled]` / `[Author Consultation Error]`),前缀字节稳定利于 Prompt Cache 命中
|
|
8
|
+
* - 性能与生命周期:调用卡片按 args 身份记忆化派生、结果卡片复用槽位 `lastComponent`;AbortSignal 经组件级联取消;本文件不注册任何监听器与定时器,`AskAuthorComponent.dispose()`(幂等)负责回收定时器、Abort 监听与全部缓存池
|
|
9
|
+
*
|
|
10
|
+
* 依赖方向:index.ts 为 11 模块单向依赖图(theme → texts → {format, schema} → {model, sanitize} → {novel-markdown, view-rows, view-summary} → component → index)的入口;旁支 `sanitize.ts` 仅由本文件装配(`prepareArguments` 与 `renderCall` 两处)
|
|
11
|
+
*/
|
|
12
|
+
import { type AgentToolResult, type ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
13
|
+
import { type AskAuthorResult, type NormalizedQuestion } from "./model.js";
|
|
14
|
+
/** `ask_author` 工具执行结果结构(基于 Pi 官方 `AgentToolResult` 结构特化) */
|
|
15
|
+
export type AskAuthorToolResult = AgentToolResult<AskAuthorResult>;
|
|
16
|
+
/** 结果构造上下文:题目快照与标题/描述的可选覆盖(缺省值统一由 {@link resolveResultContext} 收敛) */
|
|
17
|
+
export interface ResultContext {
|
|
18
|
+
/** 题目快照(缺省空数组:取消/错误可能发生在模型构建前) */
|
|
19
|
+
questions?: NormalizedQuestion[];
|
|
20
|
+
/** 问卷总标题(缺省 `TEXTS.fallbacks.defaultFormTitle`) */
|
|
21
|
+
formTitle?: string;
|
|
22
|
+
/** 问卷全局描述(缺省 undefined,不携带该字段内容) */
|
|
23
|
+
formDescription?: string;
|
|
24
|
+
}
|
|
25
|
+
/** 调用卡片派生数据:与主题无关的纯计算结果(颜色合成每帧按当前主题现场计算) */
|
|
26
|
+
export interface CallCardData {
|
|
27
|
+
/** 卡片标题(已清洗的 `formTitle`) */
|
|
28
|
+
title: string;
|
|
29
|
+
/** 计数标签(多题模式为 `2 题 • 6 选项`,单题模式仅 `6 选项`) */
|
|
30
|
+
countLabel: string;
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* `renderCall` 记忆化状态载荷:宿主按工具执行行创建一次、跨帧复用的可变对象(`ToolRenderContext.state`,同一行的 `renderCall` 与 `renderResult` 共享该实例)
|
|
34
|
+
* 以 **args 对象身份**为键(`cachedArgs` 记录身份、`cachedCard` 记录派生数据),依赖宿主「流式补全整体替换 `args` 引用」的不变量(宿主经 `ToolExecutionComponent.updateArgs` 赋入新对象):身份变化即失效重新计算;若宿主原地改写同一对象,缓存将滞留旧数据
|
|
35
|
+
*/
|
|
36
|
+
export interface CallRenderState {
|
|
37
|
+
/** 上一次派生所用的 args 对象身份(仅作 `===` 引用比对,不读取任何属性) */
|
|
38
|
+
cachedArgs?: object;
|
|
39
|
+
/** 与 `cachedArgs` 配对的派生数据;两者身份同时命中时直接复用 */
|
|
40
|
+
cachedCard?: CallCardData;
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* `ask-author` 扩展插件主入口工厂(仅完成注册,启动期不执行任何交互)
|
|
44
|
+
* @param pi - Pi 扩展运行时 API 实例
|
|
45
|
+
*/
|
|
46
|
+
export default function askAuthorExtension(pi: ExtensionAPI): void;
|
|
47
|
+
export { askAuthorExtension };
|
|
48
|
+
export { AskAuthorFormModel, type AskAuthorResult, type AuthorAnswerItem, type NormalizedQuestion } from "./model.js";
|
|
49
|
+
export { AskAuthorParams, type RawParams } from "./schema.js";
|
|
50
|
+
export { sanitizeAskAuthorArgs } from "./sanitize.js";
|
|
51
|
+
export { TEXTS, LAYOUT_CONFIG, ICONS } from "./texts.js";
|