@master.g/pi-wingman 0.0.2 → 0.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 CHANGED
@@ -10,7 +10,7 @@
10
10
  | **advisor** | oh-my-pi advisor | 指定一个模型当顾问,读主 agent 的每个 turn,以 steer 消息内联注入便签(aside / concern / blocker)让主 agent 纠偏 |
11
11
  | **hint** | Claude Code prompt suggestions | 任务结束后按上下文与目标生成下一步 prompt,输入框 ghost text 呈现:Tab 填入、Enter 直接发送 |
12
12
 
13
- > 状态:早期版本(v0.0.2)。hint、advisor、heads-up 三个功能均已实现,默认关闭,尚在试用打磨中。
13
+ > 状态:早期版本(v0.0.3)。hint、advisor、heads-up 三个功能均已实现,默认关闭,尚在试用打磨中。
14
14
 
15
15
  ## 安装
16
16
 
@@ -30,6 +30,8 @@ pi --extension ~/github/pi-wingman/index.ts
30
30
  - `/wingman advisor|hint|headsup` — 不带值时弹出 pi 的模型选择器(同 `/model`,可搜索),选完模型再选思考强度(同 `/thinking`,Esc 跳过);也可直接写 `<provider/model> [effort]`(模型须存在,effort 须是该模型支持的档位)、`on` / `off`(开关,保留已设模型与 effort);参数支持补全
31
31
  - `/ask-advisor <问题>` — 就当前上下文咨询顾问模型,回答注入对话
32
32
 
33
+ advisor 与 heads-up 审计通过(ok)时不打扰,只在底栏状态显示 `advisor ⋯`(审计中)、`advisor ✓ ok`(结果级别)或 `advisor ✗ 原因`(调用失败);自定义 footer 扩展需要自己渲染扩展状态才看得到。
34
+
33
35
  ## 配置
34
36
 
35
37
  `<agentDir>/wingman.json`(默认 `~/.pi/agent/wingman.json`),由命令自动写入,也可手改:
package/advisor.ts CHANGED
@@ -13,7 +13,7 @@
13
13
  */
14
14
 
15
15
  import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
16
- import { Auditor, buildDigest, type DigestMessage, type VerdictLevel } from "./audit.js";
16
+ import { auditStatus, Auditor, buildDigest, type DigestMessage, type VerdictLevel } from "./audit.js";
17
17
  import type { WingmanConfig } from "./config.js";
18
18
  import { completeText, messageText, resolveModel, type AnyModel, type ModelRegistry } from "./llm.js";
19
19
 
@@ -52,6 +52,8 @@ export class AdvisorState {
52
52
 
53
53
  const ASK_SYSTEM_PROMPT = `你是资深编程顾问,在旁观察一个编程 agent 的工作。给你用户的目标和 agent 近期动作摘要,以及用户现在问你的问题。直接回答问题本身,简洁具体,≤300 字,用中文。`;
54
54
 
55
+ const STATUS_KEY = "wingman-advisor";
56
+
55
57
  const NOTIFY_TYPE: Record<Exclude<VerdictLevel, "ok">, "info" | "warning" | "error"> = {
56
58
  note: "info",
57
59
  concern: "warning",
@@ -68,14 +70,19 @@ export function installAdvisor(pi: ExtensionAPI, getConfig: () => WingmanConfig,
68
70
  let pendingDigests: string[] = [];
69
71
 
70
72
  const audit = async (ctx: ExtensionContext, registry: ModelRegistry, model: AnyModel, digest: string) => {
71
- const verdict = await auditor.run(
73
+ ctx.ui.setStatus(STATUS_KEY, auditStatus("advisor"));
74
+ const result = await auditor.run(
72
75
  registry,
73
76
  model,
74
77
  { goal: getGoal(), digest },
75
78
  // 后台审计不卡主流程,超时放宽给高 effort 留余地
76
79
  { timeoutMs: 60_000, signal: shutdown.signal, effort: getConfig().advisor.effort },
77
80
  );
78
- if (!verdict || verdict.level === "ok") return;
81
+ if (!result) return;
82
+ ctx.ui.setStatus(STATUS_KEY, auditStatus("advisor", result));
83
+ if (!result.ok) return;
84
+ const { verdict } = result;
85
+ if (verdict.level === "ok") return;
79
86
  ctx.ui.notify(`advisor [${verdict.level}] ${verdict.summary}`, NOTIFY_TYPE[verdict.level]);
80
87
  if (ctx.isIdle()) return; // 裁决迟到:agent 已停,只提示用户,不注入
81
88
  pi.sendUserMessage(formatAdvisorNote(verdict), { deliverAs: "steer" });
package/audit.ts CHANGED
@@ -21,6 +21,15 @@ export interface Verdict {
21
21
  details: string[];
22
22
  }
23
23
 
24
+ /** 一次审计的结果:成功带裁决,失败带原因(给底栏状态看)。 */
25
+ export type AuditResult = { ok: true; verdict: Verdict } | { ok: false; reason: string };
26
+
27
+ /** 底栏状态文本:不传结果为审计中,否则 ✓ 级别 / ✗ 原因。 */
28
+ export function auditStatus(feature: string, result?: AuditResult): string {
29
+ if (!result) return `${feature} ⋯`;
30
+ return result.ok ? `${feature} ✓ ${result.verdict.level}` : `${feature} ✗ ${result.reason.slice(0, 60)}`;
31
+ }
32
+
24
33
  export type DigestMessage = Message;
25
34
 
26
35
  export interface DigestOptions {
@@ -93,7 +102,7 @@ export async function runAudit(
93
102
  model: AnyModel,
94
103
  input: { goal: string; digest: string },
95
104
  options: CallOptions = {},
96
- ): Promise<Verdict | undefined> {
105
+ ): Promise<AuditResult> {
97
106
  const goal = input.goal ? `用户目标:${input.goal}\n\n` : "";
98
107
  const result = await completeText(
99
108
  registry,
@@ -110,8 +119,9 @@ export async function runAudit(
110
119
  },
111
120
  options,
112
121
  );
113
- if (!result.ok) return undefined;
114
- return parseVerdict(result.text);
122
+ if (!result.ok) return result;
123
+ const verdict = parseVerdict(result.text);
124
+ return verdict ? { ok: true, verdict } : { ok: false, reason: `无法解析裁决:${result.text.slice(0, 40)}` };
115
125
  }
116
126
 
117
127
  /** 冷却 + 互斥的审计器:advisor(turn_end)与 heads-up(去抖)共用一个实例各管各的。 */
@@ -136,14 +146,14 @@ export class Auditor {
136
146
  model: AnyModel,
137
147
  input: { goal: string; digest: string },
138
148
  options: CallOptions = {},
139
- ): Promise<Verdict | undefined> {
140
- if (!this.ready) return undefined;
149
+ ): Promise<AuditResult | undefined> {
150
+ if (!this.ready) return undefined; // 冷却或在途:跳过
141
151
  this.inFlight = true;
142
152
  this.lastRunAt = this.now();
143
153
  try {
144
154
  return await runAudit(registry, model, input, options);
145
- } catch {
146
- return undefined; // 静默降级
155
+ } catch (error) {
156
+ return { ok: false, reason: error instanceof Error ? error.message : String(error) };
147
157
  } finally {
148
158
  this.inFlight = false;
149
159
  }
package/editor.ts CHANGED
@@ -2,7 +2,7 @@
2
2
  * editor.ts — WingmanEditor 与其安装管理(EditorManager)。
3
3
  *
4
4
  * WingmanEditor 是全扩展唯一的 CustomEditor(setEditorComponent 全局只能装
5
- * 一个):hint 用 ghost 行(M1),heads-up 用数字键捕获(M3)。安装条件是
5
+ * 一个):hint 用输入行内联的暗色 ghost 文本(M1),heads-up 用数字键捕获(M3)。安装条件是
6
6
  * hint.enabled || headsup.enabled;session_start 与配置命令变更后各 sync
7
7
  * 一次,条件不变不动(避免重建丢掉 pendingHint)。
8
8
  */
@@ -32,10 +32,9 @@ export class WingmanEditor extends CustomEditor {
32
32
 
33
33
  render(width: number): string[] {
34
34
  const lines = super.render(width);
35
- if (!this.pendingHint || this.getText().length > 0 || lines.length < 2) return lines;
36
- // shortcut: ghost 渲染为输入行下方新增一行;如需与输入行内联覆盖再升级。
37
- const ghost = truncateToWidth(`\x1b[2m 💡 ${this.pendingHint}\x1b[0m`, width);
38
- lines.splice(lines.length - 1, 0, ghost);
35
+ if (!this.pendingHint || this.getText().length > 0 || lines.length < 3) return lines;
36
+ // 空编辑器的唯一内容行紧跟上边框:去掉行尾填充,把 hint 以暗色接在光标后
37
+ lines[1] = truncateToWidth(`${lines[1].replace(/ +$/, "")}\x1b[2m${this.pendingHint}\x1b[22m`, width);
39
38
  return lines;
40
39
  }
41
40
 
package/headsup.ts CHANGED
@@ -20,7 +20,7 @@ import { truncateToWidth } from "@earendil-works/pi-tui";
20
20
  import { appendFileSync } from "node:fs";
21
21
  import { tmpdir } from "node:os";
22
22
  import { join } from "node:path";
23
- import { Auditor, type Verdict, type VerdictLevel } from "./audit.js";
23
+ import { auditStatus, Auditor, type Verdict, type VerdictLevel } from "./audit.js";
24
24
  import type { WingmanConfig } from "./config.js";
25
25
  import type { EditorManager } from "./editor.js";
26
26
  import { resolveModel } from "./llm.js";
@@ -31,6 +31,7 @@ const WIDGET_KEY = "wingman-headsup";
31
31
  const LEVEL_COLOR: Partial<Record<VerdictLevel, ThemeColor>> = { concern: "warning", blocker: "error" };
32
32
  /** 冷却期内最多积压的工具事件数,超出丢最旧的。 */
33
33
  const MAX_PENDING = 30;
34
+ const STATUS_KEY = "wingman-headsup";
34
35
 
35
36
  /** WINGMAN_DEBUG=1 时把管线决策追加到 <tmpdir>/wingman-debug.log;旁路默认静默,这是唯一的观测口。 */
36
37
  function debug(message: string): void {
@@ -213,14 +214,19 @@ export function installHeadsUp(
213
214
  const events = pending;
214
215
  pending = [];
215
216
  debug(`flush: ${events.length} 个事件送审计`);
216
- const verdict = await auditor.run(
217
+ ctx.ui.setStatus(STATUS_KEY, auditStatus("headsup"));
218
+ const result = await auditor.run(
217
219
  ctx.modelRegistry,
218
220
  model,
219
221
  { goal: getGoal(), digest: formatToolEvents(events) },
220
222
  { timeoutMs: 60_000, signal: shutdown.signal, effort: config.effort },
221
223
  );
222
- debug(`verdict: ${verdict ? `${verdict.level} ${verdict.summary}` : "(调用失败/跳过)"}`);
223
- if (!verdict || (verdict.level !== "concern" && verdict.level !== "blocker")) return;
224
+ if (!result) return;
225
+ ctx.ui.setStatus(STATUS_KEY, auditStatus("headsup", result));
226
+ debug(`verdict: ${result.ok ? `${result.verdict.level} ${result.verdict.summary}` : `调用失败 ${result.reason}`}`);
227
+ if (!result.ok) return;
228
+ const { verdict } = result;
229
+ if (verdict.level !== "concern" && verdict.level !== "blocker") return;
224
230
  if (!view.show(verdict)) {
225
231
  debug("已在抑制集,不上屏");
226
232
  return;
package/index.ts CHANGED
@@ -116,6 +116,7 @@ export default function (pi: ExtensionAPI) {
116
116
  if (value) {
117
117
  saveConfig(config);
118
118
  editors.sync(ctx, config);
119
+ if (!config[feature].enabled) ctx.ui.setStatus(`wingman-${feature}`, undefined);
119
120
  }
120
121
  ctx.ui.notify(`wingman · ${line}`, "info");
121
122
  },
package/llm.ts CHANGED
@@ -76,7 +76,8 @@ export async function completeText(
76
76
  .streamSimple(model, context, { signal: controller.signal, reasoning: effort === "off" ? undefined : effort })
77
77
  .result();
78
78
  if (response.stopReason !== "stop") {
79
- return { ok: false, reason: `stopReason=${response.stopReason}` };
79
+ const detail = response.errorMessage ? `: ${response.errorMessage}` : "";
80
+ return { ok: false, reason: `stopReason=${response.stopReason}${detail}` };
80
81
  }
81
82
  const text = textOf(response.content).trim();
82
83
  if (!text) return { ok: false, reason: "empty response" };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@master.g/pi-wingman",
3
- "version": "0.0.2",
3
+ "version": "0.0.3",
4
4
  "description": "Wingman for pi — heads-up audit while the agent works, a designated advisor model, and next-prompt hints (Tab/Enter to accept)",
5
5
  "type": "module",
6
6
  "license": "MIT",