@zhushanwen/pi-scheduler 0.11.1 → 0.11.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 CHANGED
@@ -28,15 +28,9 @@ npm install @zhushanwen/pi-scheduler
28
28
 
29
29
  安装后扩展在 pi 会话启动时自动装配(见下节),无需额外配置。
30
30
 
31
- ## 工具激活(deferred)
31
+ ## 工具激活
32
32
 
33
- `schedule` / `schedule_control` 工具注册为 `exposure: "deferred"`——不占常驻工具声明,由 pi 内置 `tool_search` 工具检索命中后按需激活。taiji 宿主内开箱即用(宿主启动时已激活 `tool_search`);独立 pi 使用需在 `<agentDir>/settings.json`(默认 `~/.pi/agent/settings.json`)开启:
34
-
35
- ```json
36
- { "defaultTools": ["+tool_search"] }
37
- ```
38
-
39
- `+` 前缀是增量修饰(保留 read/bash/edit/write 默认集)。未开启 `tool_search` 时这两个工具对模型不可见。
33
+ `schedule` / `schedule_control` 两工具均为 direct 常驻——注册不声明 `exposure` 字段(pi SDK 缺省 `direct`),工具声明随会话常驻、对模型默认可见,无需经 `tool_search` 检索激活,独立 pi 开箱即用。
40
34
 
41
35
  ## 激活方式
42
36
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zhushanwen/pi-scheduler",
3
- "version": "0.11.1",
3
+ "version": "0.11.2",
4
4
  "type": "module",
5
5
  "main": "index.ts",
6
6
  "taiji": {
@@ -17,7 +17,7 @@
17
17
  // 读取——若在 factory 顶层捕获 service! 非空断言,注册时 service 为 null,
18
18
  // execute 调用会 NPE。此套件验证 session_start 前 execute 优雅 throw 而非 crash。
19
19
 
20
- import type { ExtensionAPI, ExtensionContext } from '@earendil-works/pi-coding-agent'
20
+ import type { ExtensionAPI, ExtensionContext, Theme } from '@earendil-works/pi-coding-agent'
21
21
  import { describe, expect, it, vi } from 'vitest'
22
22
 
23
23
  // MF-3:session_start handler 会真实执行 importLegacyStore(ctx.cwd, ...) —— fakeCtx cwd='/test'
@@ -105,6 +105,40 @@ describe('pi-scheduler SDK contract', () => {
105
105
  }
106
106
  })
107
107
 
108
+ // exposure 重划终态锚(C-ext-05,2026-10-23 ADR-0136):两工具保持 direct 常驻 =
109
+ // 注册时不写 exposure 字段(SDK 缺省 direct,pi-semantics PS-91)——误写 "deferred" 会把
110
+ // 触发入口从模型默认可见集收走(tool_search 才可激活),此处红。
111
+ it('exposure 注册终态:schedule / schedule_control 均 direct 常驻(不写 exposure 字段,SDK 缺省)', () => {
112
+ const { pi, tools } = createMockPi()
113
+ schedulerExtension(pi)
114
+ expect(tools.map(t => t.name).sort()).toEqual(['schedule', 'schedule_control'])
115
+ for (const tool of tools) {
116
+ expect(tool.exposure).toBeUndefined()
117
+ }
118
+ })
119
+
120
+ it('每个 tool 有 renderResult 函数成员(C-ext-05)+ 折叠态输出 details.summary 一行', () => {
121
+ const { pi, tools } = createMockPi()
122
+ schedulerExtension(pi)
123
+ for (const tool of tools) {
124
+ expect(typeof tool.renderResult).toBe('function')
125
+ const renderResult = tool.renderResult as (
126
+ result: unknown,
127
+ options: { expanded: boolean },
128
+ theme: Theme,
129
+ ) => { render: (width: number) => string[] }
130
+ const renderTheme = { fg: (_token: string, text: string) => text } as unknown as Theme
131
+ const result = {
132
+ content: [{ type: 'text', text: 'schedule output…' }],
133
+ details: { summary: 'Scheduled reminder in 5m' },
134
+ }
135
+ // Text.render 按宽度 pad,trim 后断言(折叠态 = details.summary 一行)
136
+ const lines = renderResult(result, { expanded: false }, renderTheme).render(400)
137
+ expect(lines.length).toBe(1)
138
+ expect(lines[0].trim()).toBe('Scheduled reminder in 5m')
139
+ }
140
+ })
141
+
108
142
  it('execute 是 async function', () => {
109
143
  const { pi, tools } = createMockPi()
110
144
  schedulerExtension(pi)
@@ -47,6 +47,13 @@ describe('buildScheduleSummary', () => {
47
47
  const r = toolResult({ task: { name: 'Odd', schedule: { mode: 'bogus' } } })
48
48
  expect(buildScheduleSummary(createParams, r)).toBe('Scheduled "Odd" · 5m')
49
49
  })
50
+
51
+ it('name / schedule 含换行 → 摘要归一单行(S-1)', () => {
52
+ const r = toolResult({ task: { name: 'Multi\nline', schedule: { mode: 'bogus' } } })
53
+ expect(buildScheduleSummary({ ...createParams, schedule: '5m\nextra' }, r)).toBe(
54
+ 'Scheduled "Multi line" · 5m extra',
55
+ )
56
+ })
50
57
  })
51
58
 
52
59
  // ── buildScheduleControlSummary ──────────────────────
package/src/format.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  // 时间格式化器(r4 M1 locale 版):formatSchedule / formatRelativeTime 是 locale 必填的
2
- // 本地实现(UiLocale 双语输出,i18n.ts / widget / service / commands 消费);fix 轨 locale
2
+ // 本地实现(UiLocale 双语输出,i18n.ts / widget / service / commands 消费);固定版 locale
3
3
  // 化与 U2 protocol 下沉(无 locale 面,服务 taiji plugin 管理侧)暂双轨并存,收敛待后续
4
4
  // 把 locale 版提升进 @zhushanwen/extension-protocol。formatDuration / MS_PER_* 单源
5
5
  // @zhushanwen/extension-protocol(re-export 保持既有 import 路径)。
package/src/index.ts CHANGED
@@ -260,9 +260,6 @@ export default function schedulerExtension(pi: ExtensionAPI): void {
260
260
  pi.registerTool({
261
261
  name: 'schedule',
262
262
  label: 'Schedule',
263
- // deferred(tool-declaration-slimming D4):不常驻声明,tool_search 命中即激活;
264
- // 迁移写入的 +tool_search 条目(runtime pi-codemode-settings)承载检索入口
265
- exposure: 'deferred',
266
263
  description:
267
264
  'Create a scheduled task that fires a message at intervals or cron schedule. ' +
268
265
  'The task is created immediately (no confirmation form). It is a session-scoped reminder, ' +
@@ -295,8 +292,6 @@ export default function schedulerExtension(pi: ExtensionAPI): void {
295
292
  pi.registerTool({
296
293
  name: 'schedule_control',
297
294
  label: 'Schedule Control',
298
- // deferred(tool-declaration-slimming D4):同 schedule
299
- exposure: 'deferred',
300
295
  description: 'Manage scheduled tasks: list, toggle, delete, or run immediately.',
301
296
  parameters: ScheduleControlParams,
302
297
  promptGuidelines: controlGuidelines,
@@ -7,32 +7,7 @@
7
7
 
8
8
  import type { Theme } from '@earendil-works/pi-coding-agent'
9
9
  import { Text } from '@earendil-works/pi-tui'
10
- import { firstContentText } from '@zhushanwen/extension-protocol'
11
-
12
- /** content[0] 的 text 纯文本(content 缺失或非 text 类型时兜底空串)。 */
13
- function contentText(result: unknown): string {
14
- const content = fieldValue(result, 'content')
15
- if (!Array.isArray(content)) return ''
16
- return firstContentText({ content })
17
- }
18
-
19
- /** unknown 值按字段名安全取值(index-signature 收窄,替代全可选结构断言)。 */
20
- function fieldValue(v: unknown, key: string): unknown {
21
- if (typeof v !== 'object' || v === null) return undefined
22
- return (v as Record<string, unknown>)[key]
23
- }
24
-
25
- /** 从 details 安全读取 summary(容错非对象形态与旧版本 entry)。 */
26
- function readDetailsSummary(result: unknown): string | undefined {
27
- const summary = fieldValue(fieldValue(result, 'details'), 'summary')
28
- return typeof summary === 'string' && summary !== '' ? summary : undefined
29
- }
30
-
31
- /** 折叠态回退:content 文本首行(保持一行摘要形态)。 */
32
- function firstLine(text: string): string {
33
- const idx = text.indexOf('\n')
34
- return idx === -1 ? text : text.slice(0, idx)
35
- }
10
+ import { firstLine, readDetailsSummary, resultContentText } from '@zhushanwen/extension-protocol'
36
11
 
37
12
  /** 折叠态(expanded=false)= details.summary 一行(dim);展开态 = content 纯文本。 */
38
13
  export function renderScheduleToolResult(
@@ -40,7 +15,7 @@ export function renderScheduleToolResult(
40
15
  options: { expanded: boolean },
41
16
  theme: Theme,
42
17
  ): Text {
43
- if (options.expanded) return new Text(contentText(result), 0, 0)
44
- const summary = readDetailsSummary(result) ?? firstLine(contentText(result))
18
+ if (options.expanded) return new Text(resultContentText(result), 0, 0)
19
+ const summary = readDetailsSummary(result) ?? firstLine(resultContentText(result))
45
20
  return new Text(theme.fg('dim', summary), 0, 0)
46
21
  }
package/src/tool.ts CHANGED
@@ -188,6 +188,13 @@ export function withScheduleSummary<T extends ToolResultLike>(
188
188
  return { ...result, details: { ...details, summary } }
189
189
  }
190
190
 
191
+ /** 多行文本 → 单行摘要文本(换行与连续空白折叠为单空格;plan tool.ts 同模式)。
192
+ * details.summary 单行契约见 docs/extensions/extension-conventions.md
193
+ * 「Tool result details.summary 约定」(自由文本 name/schedule 可含 \n,进摘要前先归一)。 */
194
+ function toSingleLine(text: string): string {
195
+ return text.replace(/\s+/g, ' ').trim()
196
+ }
197
+
191
198
  /** schedule(创建)摘要:`Scheduled "<name>" · <schedule>`;取消态与数据缺失时按形态降级。 */
192
199
  export function buildScheduleSummary(params: ScheduleParamsT, result: ToolResultLike): string {
193
200
  const details = asRecord(result.details)
@@ -197,8 +204,8 @@ export function buildScheduleSummary(params: ScheduleParamsT, result: ToolResult
197
204
  if (name === undefined) return 'Scheduled'
198
205
  const spec = asScheduleSpec(task.schedule)
199
206
  const kind = task.kind === 'once' || task.kind === 'recurring' ? (task.kind as TaskKind) : undefined
200
- const scheduleText = spec !== undefined ? formatSchedule(spec, kind, 'en-US') : params.schedule
201
- return `Scheduled "${name}" · ${scheduleText}`
207
+ const scheduleText = spec !== undefined ? formatSchedule(spec, kind, 'en-US') : toSingleLine(params.schedule)
208
+ return `Scheduled "${toSingleLine(name)}" · ${scheduleText}`
202
209
  }
203
210
 
204
211
  /** schedule_control 摘要:按 action 分支(toggle 按 enabled 目标态分 Paused/Resumed)。 */