pi-cc-extensions 0.8.63 → 0.8.64

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.
@@ -200,12 +200,58 @@ function cachePreview(key: string, entry: PreviewCacheEntry): void {
200
200
 
201
201
  export function clearThinkingPreviewCache(): void {
202
202
  previewCache.clear();
203
+ expandedWrapCache.clear();
204
+ }
205
+
206
+ type ExpandedWrapEntry = { text: string; lines: string[] };
207
+ const expandedWrapCache = new Map<string, ExpandedWrapEntry>();
208
+ export const THINKING_EXPANDED_WRAP_CACHE_MAX = 64;
209
+
210
+ function expandedWrapKey(messageTimestamp: number, runStartIndex: number, width: number): string {
211
+ return `${messageTimestamp}:${runStartIndex}:${width}`;
212
+ }
213
+
214
+ function wrapExpandedThinking(
215
+ messageTimestamp: number,
216
+ runStartIndex: number,
217
+ text: string,
218
+ width: number,
219
+ ): string[] {
220
+ const key = expandedWrapKey(messageTimestamp, runStartIndex, width);
221
+ const hit = expandedWrapCache.get(key);
222
+ if (hit && hit.text === text) {
223
+ expandedWrapCache.delete(key);
224
+ expandedWrapCache.set(key, hit);
225
+ return hit.lines;
226
+ }
227
+ const lines = wrapTextWithAnsi(text.replace(/\t/g, " "), Math.max(1, width));
228
+ expandedWrapCache.delete(key);
229
+ while (expandedWrapCache.size >= THINKING_EXPANDED_WRAP_CACHE_MAX) {
230
+ const oldest = expandedWrapCache.keys().next().value;
231
+ if (oldest === undefined) break;
232
+ expandedWrapCache.delete(oldest);
233
+ }
234
+ expandedWrapCache.set(key, { text, lines });
235
+ return lines;
236
+ }
237
+
238
+ function evictExpandedWraps(messageTimestamp: number, runStartIndex: number): void {
239
+ for (const key of expandedWrapCache.keys()) {
240
+ const [ts, run] = key.split(":");
241
+ if (Number(ts) === messageTimestamp && Number(run) === runStartIndex) {
242
+ expandedWrapCache.delete(key);
243
+ }
244
+ }
203
245
  }
204
246
 
205
247
  export function thinkingPreviewCacheSize(): number {
206
248
  return previewCache.size;
207
249
  }
208
250
 
251
+ export function thinkingExpandedWrapCacheSize(): number {
252
+ return expandedWrapCache.size;
253
+ }
254
+
209
255
  /** 按可见宽度估算折行数,不走 Text 全量 wrap。无换行长段也能计到隐藏行。 */
210
256
  function countWrappedLines(text: string, contentWidth: number): number {
211
257
  if (!text) return 0;
@@ -285,12 +331,12 @@ export class ThinkingPreviewBlock implements Component {
285
331
  private heading: string;
286
332
  private text: string;
287
333
  private padding: number;
288
- private messageTimestamp: number;
334
+ readonly messageTimestamp: number;
335
+ readonly runStartIndex: number;
289
336
  private style: (text: string) => string;
290
337
  private theme: Theme | undefined;
291
338
  private _expanded: boolean;
292
339
  private hintHovered = false;
293
- private expandedBody: { width: number; lines: string[] } | undefined;
294
340
  private collapsedMemo:
295
341
  | {
296
342
  width: number;
@@ -308,11 +354,13 @@ export class ThinkingPreviewBlock implements Component {
308
354
  messageTimestamp: number,
309
355
  style: (text: string) => string,
310
356
  theme?: Theme,
357
+ runStartIndex = 0,
311
358
  ) {
312
359
  this.heading = heading;
313
360
  this.text = text;
314
361
  this.padding = padding;
315
362
  this.messageTimestamp = messageTimestamp;
363
+ this.runStartIndex = runStartIndex;
316
364
  this.style = style;
317
365
  this.theme = theme;
318
366
  this._expanded = expandedThinking.has(messageTimestamp);
@@ -334,7 +382,7 @@ export class ThinkingPreviewBlock implements Component {
334
382
  if (expanded) expandedThinking.add(this.messageTimestamp);
335
383
  else {
336
384
  expandedThinking.delete(this.messageTimestamp);
337
- this.expandedBody = undefined;
385
+ evictExpandedWraps(this.messageTimestamp, this.runStartIndex);
338
386
  }
339
387
  }
340
388
 
@@ -368,13 +416,10 @@ export class ThinkingPreviewBlock implements Component {
368
416
  return { lines: [], hiddenLines: 0 };
369
417
  }
370
418
  if (this._expanded) {
371
- if (this.expandedBody?.width !== width) {
372
- this.expandedBody = {
373
- width,
374
- lines: wrapTextWithAnsi(this.text.replace(/\t/g, " "), Math.max(1, width)),
375
- };
376
- }
377
- return { lines: this.expandedBody.lines, hiddenLines: 0 };
419
+ return {
420
+ lines: wrapExpandedThinking(this.messageTimestamp, this.runStartIndex, this.text, width),
421
+ hiddenLines: 0,
422
+ };
378
423
  }
379
424
  const preview = layoutThinkingPreview(this.text, width, padding);
380
425
  return { lines: preview.visible, hiddenLines: preview.hiddenLines };
@@ -430,7 +475,6 @@ export class ThinkingPreviewBlock implements Component {
430
475
 
431
476
  invalidate() {
432
477
  this.collapsedMemo = undefined;
433
- this.expandedBody = undefined;
434
478
  }
435
479
  }
436
480
 
@@ -715,6 +759,7 @@ function compactThinking(pi: ExtensionAPI) {
715
759
  message.timestamp,
716
760
  thinkingStyle,
717
761
  activeTheme,
762
+ runStartIndex,
718
763
  ),
719
764
  );
720
765
 
@@ -5,7 +5,6 @@ import { config } from "./config/config.ts";
5
5
  import piAliases from "./feature/shell/aliases.ts";
6
6
  import piStartupHeader from "./feature/shell/startup-header.ts";
7
7
  import workingMessage from "./feature/shell/working-message.ts";
8
- import modelStatus from "./feature/shell/model-status.ts";
9
8
 
10
9
  // feature
11
10
  import agentAutocomplete from "./feature/reference/subagent.ts";
@@ -23,7 +22,6 @@ export default function (pi: ExtensionAPI): void {
23
22
  if (config.enableAliases) piAliases(pi);
24
23
  piStartupHeader(pi);
25
24
  if (config.enableWorkingMessage) workingMessage(pi);
26
- modelStatus(pi);
27
25
 
28
26
  // render stack:thinking controller 直接交给 style 作 query
29
27
  markdownEnhance(pi);
@@ -54,6 +54,16 @@ export function setHoveredThinking(block: ThinkingPreviewBlock | null): boolean
54
54
  return true;
55
55
  }
56
56
 
57
+ let hoveredMessageDisplay: any = null;
58
+
59
+ export function setHoveredMessageDisplay(component: any): boolean {
60
+ if (component === hoveredMessageDisplay) return false;
61
+ hoveredMessageDisplay?.setHintHovered?.(false);
62
+ hoveredMessageDisplay = component;
63
+ component?.setHintHovered?.(true);
64
+ return true;
65
+ }
66
+
57
67
  let hoveredToolIoView: ExpandedToolIoView | null = null;
58
68
  let hoveredToolIoSection: ToolIoSection | null = null;
59
69
 
@@ -110,6 +120,7 @@ export type FullscreenHoverTarget =
110
120
  | { kind: "button" }
111
121
  | { kind: "group"; component: ToolGroupComponent }
112
122
  | { kind: "thinking"; component: ThinkingPreviewBlock }
123
+ | { kind: "message"; component: any }
113
124
  | { kind: "assistant"; component: any }
114
125
  | {
115
126
  kind: "tool";
@@ -133,6 +144,8 @@ export function applyFullscreenHover(tui: any, target: FullscreenHoverTarget | n
133
144
  if (setHoveredToolGroup(nextGroup)) changed = true;
134
145
  const nextThinking = target?.kind === "thinking" ? target.component : null;
135
146
  if (setHoveredThinking(nextThinking)) changed = true;
147
+ const nextMessage = target?.kind === "message" ? target.component : null;
148
+ if (setHoveredMessageDisplay(nextMessage)) changed = true;
136
149
  const nextAssistant = target?.kind === "assistant" ? target.component : null;
137
150
  if (setHoveredCompactAssistant(nextAssistant)) changed = true;
138
151
  const nextView = target?.kind === "tool" ? target.view : null;
@@ -59,6 +59,7 @@ import {
59
59
  setHoveredToolCallId,
60
60
  setHoveredToolGroup,
61
61
  setHoveredThinking,
62
+ setHoveredMessageDisplay,
62
63
  setHoveredToolIo,
63
64
  type FullscreenHoverTarget,
64
65
  } from "./hover.ts";
@@ -155,13 +156,20 @@ function updateToolSummaryHover(tui: any, packet: SgrMousePacket): void {
155
156
  }
156
157
 
157
158
  const EXPAND_PANEL_DOUBLE_CLICK_MS = 400;
158
- let lastExpandPanelClick: { card: any; at: number } | null = null;
159
+ let lastExpandPanelClick: { id: unknown; at: number } | null = null;
160
+
161
+ function expandPanelIdentity(card: any): unknown {
162
+ return card instanceof ThinkingPreviewBlock
163
+ ? `${card.messageTimestamp}:${card.runStartIndex}`
164
+ : card;
165
+ }
159
166
 
160
167
  function isExpandPanelDoubleClick(card: any): boolean {
161
168
  const now = Date.now();
169
+ const id = expandPanelIdentity(card);
162
170
  const prev = lastExpandPanelClick;
163
- lastExpandPanelClick = { card, at: now };
164
- return Boolean(prev && prev.card === card && now - prev.at <= EXPAND_PANEL_DOUBLE_CLICK_MS);
171
+ lastExpandPanelClick = { id, at: now };
172
+ return Boolean(prev && prev.id === id && now - prev.at <= EXPAND_PANEL_DOUBLE_CLICK_MS);
165
173
  }
166
174
 
167
175
  function clearExpandPanelDoubleClick(): void {
@@ -174,6 +182,7 @@ function collapseExpandedCard(tui: any, card: any): boolean {
174
182
  setHoveredToolCallId(null);
175
183
  setHoveredToolGroup(null);
176
184
  setHoveredThinking(null);
185
+ setHoveredMessageDisplay(null);
177
186
  setHoveredToolIo(null, null);
178
187
  setHoveredCompactAssistant(null);
179
188
  card.invalidate?.();
@@ -345,6 +354,7 @@ function handleFullscreenToolClick(tui: any, packet: SgrMousePacket): boolean {
345
354
  setHoveredToolCallId(null);
346
355
  setHoveredToolGroup(null);
347
356
  setHoveredThinking(null);
357
+ setHoveredMessageDisplay(null);
348
358
  setHoveredToolIo(null, null);
349
359
  setHoveredCompactAssistant(null);
350
360
  card.invalidate?.();
@@ -416,8 +426,7 @@ function handleFullscreenToolHover(tui: any, packet: SgrMousePacket): void {
416
426
  // compact 摘要行:折叠时仅提示文字高亮,展开卡整体高亮。
417
427
  if (component.expanded || overHint) target = { kind: "assistant", component };
418
428
  } else if (isMessageDisplayComponent(component)) {
419
- // skill/compaction/branch:折叠只点 hint;展开后双击收起。
420
- if (component.expanded || overHint) target = { kind: "assistant", component };
429
+ if (overHint) target = { kind: "message", component };
421
430
  }
422
431
  }
423
432
  }
@@ -814,6 +823,7 @@ export function teardownToolMouseInteraction(
814
823
  setHoveredToolCallId(null);
815
824
  setHoveredToolGroup(null);
816
825
  setHoveredThinking(null);
826
+ setHoveredMessageDisplay(null);
817
827
  setHoveredToolIo(null, null);
818
828
  setHoveredCompactAssistant(null);
819
829
  clearExpandPanelDoubleClick();
@@ -841,6 +851,7 @@ export function teardownToolMouseInteraction(
841
851
  export function resetToolHoverState(): void {
842
852
  setHoveredToolCallId(null);
843
853
  setHoveredThinking(null);
854
+ setHoveredMessageDisplay(null);
844
855
  setHoveredCompactAssistant(null);
845
856
  setScrollButtonVisible(false);
846
857
  setScrollButtonHovered(false);
@@ -55,6 +55,15 @@ const BRANCH_KIND: DisplayKind = {
55
55
  body: (component) => String(component.message?.summary ?? ""),
56
56
  };
57
57
 
58
+ function ensureHintHover(component: any): void {
59
+ if (typeof component.setHintHovered === "function") return;
60
+ component.setHintHovered = function (this: any, hovered: boolean) {
61
+ if (this.hintHovered === hovered) return;
62
+ this.hintHovered = hovered;
63
+ this.invalidate?.();
64
+ };
65
+ }
66
+
58
67
  function renderCcstyle(component: any, kind: DisplayKind): void {
59
68
  const theme = displayTheme;
60
69
  if (!theme) return; // 主题未就绪时保留原生渲染
@@ -64,13 +73,21 @@ function renderCcstyle(component: any, kind: DisplayKind): void {
64
73
  component.paddingY = 0; // 工具组件 paddingY=0;原生 Box 默认 1,会上下各留一个空行
65
74
  }
66
75
  component.clear();
76
+ ensureHintHover(component);
67
77
  const icon = `${BRIGHT_GREEN}✓${ANSI_FG_RESET}`; // 已完成消息,等同工具成功态
68
78
  const title = theme.fg("toolTitle", kind.title(component));
69
79
  if (!component.expanded) {
70
- const hint = theme.fg("dim", ` • ${showMoreHintText()}`);
80
+ const hovered = component.hintHovered === true;
81
+ const hint = `${theme.fg("dim", " • ")}${theme.fg(hovered ? "text" : "dim", showMoreHintText())}`;
71
82
  component.addChild(new Text(`${icon} ${title}${hint}`, 0, 0));
72
83
  return;
73
84
  }
85
+ // 展开卡与 tool 一致:userMessageBg + 上下左右 1 格
86
+ component.paddingX = 1;
87
+ component.paddingY = 1;
88
+ if (typeof theme.bg === "function" && typeof component.setBgFn === "function") {
89
+ component.setBgFn((text: string) => theme.bg("userMessageBg", text));
90
+ }
74
91
  component.addChild(new Text(`${icon} ${title}`, 0, 0));
75
92
  component.addChild(new Spacer(1));
76
93
  component.addChild(
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-cc-extensions",
3
- "version": "0.8.63",
3
+ "version": "0.8.64",
4
4
  "license": "MIT",
5
5
  "description": "A Pi productivity suite with Claude Code-style UI, context inspection, and agent/session references",
6
6
  "type": "module",
@@ -1,19 +0,0 @@
1
- import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
2
-
3
- function modelLabel(model: any): string | undefined {
4
- if (!model) return undefined;
5
- const name = model.name || model.id;
6
- if (!name) return undefined;
7
- return model.provider ? `${model.provider} · ${name}` : name;
8
- }
9
-
10
- function apply(ctx: any): void {
11
- if (!ctx?.hasUI) return;
12
- ctx.ui?.setStatus?.("model", modelLabel(ctx.model));
13
- }
14
-
15
- /** 用 setStatus 挂模型名,不 setFooter,不接管原生 footer。 */
16
- export default function (pi: ExtensionAPI): void {
17
- pi.on("session_start", async (_event, ctx) => apply(ctx));
18
- pi.on("model_select", async (_event, ctx) => apply(ctx));
19
- }