pi-cc-extensions 0.9.12 → 1.0.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.
@@ -15,11 +15,13 @@ import {
15
15
  } from "../utils/patch-keys.ts";
16
16
  import { isToolCallHovered } from "./mouse/hover.ts";
17
17
  import {
18
- countLines,
18
+ collapsedResultSummary,
19
+ formatToolDuration,
19
20
  hasExpandableDetail,
20
21
  insetComponent,
22
+ isExpandedToolIoView,
23
+ isLiveToolCall,
21
24
  isToolExpanded,
22
- outputLineCount,
23
25
  pendingIcon,
24
26
  renderCollapsedToolResultToWidth,
25
27
  renderExpandedToolResult,
@@ -31,20 +33,26 @@ import {
31
33
  toolIconColor,
32
34
  toolViewportWidth,
33
35
  } from "./tool/result.ts";
34
- import { oneLine } from "../utils/format.ts";
35
36
  import { showMoreHintText } from "./tool/show-more-hint.ts";
37
+ import {
38
+ ASK_USER_QUESTION_TOOL_NAME,
39
+ askUserExpandedBody,
40
+ askUserQuestionCount,
41
+ createAskUserResultComponent,
42
+ } from "./tool/ask-user-question.ts";
36
43
  import {
37
44
  CODEMODE_TOOL_NAME,
38
45
  codemodeExpandedBody,
39
46
  createCodemodeResultComponent,
40
47
  } from "./tool/codemode.ts";
41
- import { countWriteDiffStats } from "./tool/diff/diff-renderer.ts";
48
+ import { countWriteDiffStats, writeStatsInCollapsedBody } from "./tool/diff/diff-renderer.ts";
42
49
  import { renderRichToolResult, type WriteExecutionMetadataStore } from "./tool/diff/index.ts";
43
50
  import { getMessageDisplayTheme } from "./tool/message-display.ts";
44
51
  import { mcpToolTitle } from "./tool/mcp-title.ts";
45
52
  import {
46
53
  fitToolCallSummary,
47
54
  humanizeToolLabel,
55
+ SHELL_TOOL_NAMES,
48
56
  renderToolSummary,
49
57
  toolCallSummary,
50
58
  } from "./tool/names.ts";
@@ -95,6 +103,9 @@ type GlobalToolRenderPatch = {
95
103
  renderPaint?: (width: number) => string[];
96
104
  invalidatePaint?: () => void;
97
105
  paintCache?: WeakMap<object, ToolPaintHit>;
106
+ /** 给渲染上下文补 result;卸下时还原。 */
107
+ originalGetRenderContext?: (...args: any[]) => any;
108
+ getRenderContext?: (this: any, ...args: any[]) => any;
98
109
  };
99
110
 
100
111
  type ToolExpandedBackgroundPatch = {
@@ -213,6 +224,27 @@ function renderExpandedTaskResult(
213
224
  return formatted ? new Text(`↳ ${formatted}`, 0, 0) : undefined;
214
225
  }
215
226
 
227
+ /**
228
+ * shell 的耗时文本。Pi 原生 shell 渲染器会在结果底部报一行,ccstyle 的通用折叠行把它盖掉了。
229
+ *
230
+ * 两条路都要:新 Pi 在渲染上下文里给 `durationMs`(单调,restore 出来的行也带得到),优先用它;
231
+ * 拿不到(旧 Pi、无耗时的结果)再退回自己的时钟——开始时间在 renderCall 记,结束时间在结果定型时定一次。
232
+ * 运行中的实时计时也只能走时钟。
233
+ */
234
+ function shellDurationText(toolName: string, context: any, isPartial: boolean): string {
235
+ if (!SHELL_TOOL_NAMES.has(toolName)) return "";
236
+ const recorded = context?.durationMs;
237
+ if (!isPartial && typeof recorded === "number" && Number.isFinite(recorded) && recorded >= 0) {
238
+ return formatToolDuration(recorded);
239
+ }
240
+ const startedAt = context?.state?.ccstyleShellStartedAt;
241
+ if (typeof startedAt !== "number") return "";
242
+ const endedAt = context?.state?.ccstyleShellEndedAt;
243
+ const end = typeof endedAt === "number" ? endedAt : Date.now();
244
+ // 时钟回拨时别吐负数;快照固定时钟也会走到这。
245
+ return formatToolDuration(Math.max(0, end - startedAt));
246
+ }
247
+
216
248
  /** 用 ccstyle call/result 包装任意工具定义。 */
217
249
  function createCcstyleTool(
218
250
  originalTool: any,
@@ -232,11 +264,24 @@ function createCcstyleTool(
232
264
  return renderDefault(originalTool, "renderCall", [args, theme, context], String(toolName));
233
265
  }
234
266
 
267
+ // shell 的耗时从执行开始算;restore 出来的行没有 executionStarted,不参与。
268
+ if (
269
+ SHELL_TOOL_NAMES.has(toolName) &&
270
+ context?.executionStarted &&
271
+ context.state &&
272
+ context.state.ccstyleShellStartedAt === undefined
273
+ ) {
274
+ context.state.ccstyleShellStartedAt = Date.now();
275
+ }
276
+
235
277
  const visualState = resolveToolVisualState(context);
236
278
  const isPending =
237
279
  visualState === "pending" ||
238
280
  (!visualState && (context?.isPartial || context?.executionStarted));
239
- const animating = isPending && Boolean(context?.executionStarted);
281
+ // 等待窗口(声明流式中或等待派发)也要转:Pi 要到 assistant 消息结束
282
+ // 才派发执行,这段只靠 executionStarted 会一直冻在第一帧。
283
+ const animating =
284
+ isPending && (Boolean(context?.executionStarted) || isLiveToolCall(context?.toolCallId));
240
285
  if (animating) scheduleAnimation(context, { light: true });
241
286
  const settledRawIcon = isPending ? "" : settledIcon(toolName, visualState);
242
287
  const settledIconStyled =
@@ -251,9 +296,23 @@ function createCcstyleTool(
251
296
  variant: "default",
252
297
  cwd: context?.cwd,
253
298
  });
299
+ // ask_user_question:问题原文在折叠结果树里逐条列出,标题只留工具名和问题数,
300
+ // 否则第一个问题会在标题和树里各出现一次。
301
+ if (toolName === ASK_USER_QUESTION_TOOL_NAME) {
302
+ // Pi 的渲染上下文不带 result,问题数从这里取。
303
+ const questionCount = askUserQuestionCount(context?.result, args);
304
+ summary.main = title;
305
+ summary.detail = questionCount === undefined ? "" : ` (+${questionCount})`;
306
+ summary.payload = undefined;
307
+ }
254
308
  let writeStatsText = "";
255
309
  let writeStatsStyled = "";
256
- if (toolName === "write" && visualState === "success") {
310
+ // 折叠 0 行预览时计数由正文那行(↳ diff +A -D · created …)显示,标题不重复。
311
+ const statsInBody = writeStatsInCollapsedBody(
312
+ Boolean(context?.expanded),
313
+ getToolDisplayConfig(),
314
+ );
315
+ if (toolName === "write" && visualState === "success" && !statsInBody) {
257
316
  const meta = writeExecutionMetadata.get(context?.toolCallId);
258
317
  const stats = countWriteDiffStats(
259
318
  typeof args?.content === "string" ? args.content : undefined,
@@ -336,11 +395,62 @@ function createCcstyleTool(
336
395
  isHovered: () => isToolCallHovered(toolCallId),
337
396
  });
338
397
  }
398
+ // ask_user_question:折叠态摊成问答树,展开态按问题逐条列出选择。
399
+ if (toolName === ASK_USER_QUESTION_TOOL_NAME) {
400
+ if (!options?.isPartial) setToolVisualState(context, isError ? "error" : "success");
401
+ if (options?.isPartial) {
402
+ // 折叠态 1 格缩进:与完成后的 ├/└ 对齐标题行的勾。
403
+ const pending = expanded ? "↳ Pending…" : " ↳ Pending…";
404
+ return new Text(theme.fg("muted", pending), 0, 0);
405
+ }
406
+ if (expanded) {
407
+ return renderExpandedToolResult(
408
+ askUserExpandedBody(result),
409
+ theme,
410
+ isError,
411
+ context?.lastComponent,
412
+ context?.args,
413
+ context,
414
+ true,
415
+ );
416
+ }
417
+ if (context?.state) context.state.ccstyleIoView = undefined;
418
+ const toolCallId = context?.toolCallId;
419
+ return createAskUserResultComponent({
420
+ result,
421
+ theme,
422
+ isError,
423
+ isHovered: () => isToolCallHovered(toolCallId),
424
+ });
425
+ }
339
426
  if (options?.isPartial) {
340
- // 展开态贴左(Box 提供 1 格 pad);折叠态保持 3 格对齐标题
427
+ // 折叠态保持 3 格:与完成后的 ` ↳ …` 同列,工具跑完那行不横跳。
428
+ // 展开态贴左(Box 提供 1 格 pad)。
341
429
  const pending = expanded ? "↳ Pending…" : " ↳ Pending…";
342
- return new Text(theme.fg("muted", pending), 0, 0);
430
+ if (!SHELL_TOOL_NAMES.has(toolName)) {
431
+ return new Text(theme.fg("muted", pending), 0, 0);
432
+ }
433
+ // shell 运行中报已用时间:数字在 render() 内取,动画 tick 才能带上新值。
434
+ return {
435
+ render(width: number) {
436
+ const elapsed = shellDurationText(toolName, context, true);
437
+ const suffix = elapsed ? theme.fg("dim", ` · ${elapsed}`) : "";
438
+ return [truncateToWidth(theme.fg("muted", pending) + suffix, width, "")];
439
+ },
440
+ invalidate(): void {},
441
+ };
442
+ }
443
+
444
+ // shell:结果定型时定一次结束时间,这一行之后不再变。
445
+ if (
446
+ SHELL_TOOL_NAMES.has(toolName) &&
447
+ context?.state &&
448
+ context.state.ccstyleShellEndedAt === undefined
449
+ ) {
450
+ context.state.ccstyleShellEndedAt = Date.now();
343
451
  }
452
+ // 折叠行接在摘要后,展开态接在 Output 正文后(原生也是在结果底部报一行)。
453
+ const duration = shellDurationText(toolName, context, false);
344
454
 
345
455
  setToolVisualState(context, isError ? "error" : "success");
346
456
  const toolCallId = context?.toolCallId;
@@ -371,26 +481,16 @@ function createCcstyleTool(
371
481
  if (taskResult) return taskResult;
372
482
  }
373
483
  const tasks = !isError && toolName === "TaskList" ? parseTaskList(text) : [];
374
- const outputLines = outputLineCount(result) || countLines(text);
375
- const lineWord = outputLines === 1 ? "line" : "lines";
376
- const action = toolName === "read" ? "loaded" : "returned";
377
484
  const rendered = tasks.length
378
485
  ? taskListSummary(tasks)
379
- : isError
380
- ? text
381
- ? // 72 与原 result.ts 默认一致,保持错误摘要截断宽度。
382
- oneLine(text, 72)
383
- : "Failed"
384
- : outputLines
385
- ? `${outputLines} ${lineWord} ${action}`
386
- : "Done";
486
+ : collapsedResultSummary({ result, toolName, text, isError });
387
487
  const expandable = !expanded && (tasks.length > 0 || hasExpandableDetail(text, args));
388
488
  const hintText = showMoreHintText();
389
489
  const hintPrefix = expandable ? theme.fg("dim", " · ") : "";
390
490
  const hint = expandable ? hintPrefix + theme.fg("dim", hintText) : "";
391
491
  const hoveredHint = expandable ? hintPrefix + theme.fg("text", hintText) : "";
392
492
  if (expanded) {
393
- return renderExpandedToolResult(
493
+ const view = renderExpandedToolResult(
394
494
  text || "",
395
495
  theme,
396
496
  Boolean(isError),
@@ -399,8 +499,14 @@ function createCcstyleTool(
399
499
  context,
400
500
  true, // mode=on:贴左,由外层 Box(1,1) 提供 1 格 padding
401
501
  );
502
+ // 尾部附注只能挂在 IoView 上:鼠标层按对象识别它,包一层会断掉命中。
503
+ // 与折叠态保持同一写法:裸值,不写 Took。
504
+ if (isExpandedToolIoView(view)) view.setFooter(duration || undefined);
505
+ return view;
402
506
  }
403
507
  if (context?.state) context.state.ccstyleIoView = undefined;
508
+ // 折叠态的耗时接在摘要后、展开提示前(提示必须留在行尾供鼠标层识别)。
509
+ const body = duration ? `${rendered}${theme.fg("dim", ` · ${duration}`)}` : rendered;
404
510
  let cachedWidth: number | undefined;
405
511
  let cachedLine: string | undefined;
406
512
  let cachedHoveredLine: string | undefined;
@@ -410,11 +516,11 @@ function createCcstyleTool(
410
516
  cachedWidth = width;
411
517
  cachedLine = theme.fg(
412
518
  isError ? "error" : "muted",
413
- renderCollapsedToolResultToWidth(rendered, hint, width),
519
+ renderCollapsedToolResultToWidth(body, hint, width),
414
520
  );
415
521
  cachedHoveredLine = theme.fg(
416
522
  isError ? "error" : "muted",
417
- renderCollapsedToolResultToWidth(rendered, hoveredHint, width),
523
+ renderCollapsedToolResultToWidth(body, hoveredHint, width),
418
524
  );
419
525
  }
420
526
  return [isToolCallHovered(toolCallId) ? cachedHoveredLine! : cachedLine];
@@ -566,6 +672,10 @@ function installGlobalToolRendering(
566
672
  previous?.originalInvalidate && prototype.invalidate === previous.invalidatePaint
567
673
  ? previous.originalInvalidate
568
674
  : prototype.invalidate;
675
+ const originalGetRenderContext =
676
+ previous?.originalGetRenderContext && prototype.getRenderContext === previous.getRenderContext
677
+ ? previous.originalGetRenderContext
678
+ : prototype.getRenderContext;
569
679
  if (isOwnershipAwarePatch(previous)) disconnectGlobalToolRenderPatch(previous);
570
680
 
571
681
  const patch: GlobalToolRenderPatch = {
@@ -581,6 +691,7 @@ function installGlobalToolRendering(
581
691
  installed: undefined as any,
582
692
  originalRender,
583
693
  originalInvalidate,
694
+ originalGetRenderContext,
584
695
  paintCache: new WeakMap(),
585
696
  };
586
697
 
@@ -618,6 +729,13 @@ function installGlobalToolRendering(
618
729
  prototype.getRenderShell = patch.installed.getRenderShell;
619
730
  prototype.getCallRenderer = patch.installed.getCallRenderer;
620
731
  prototype.getResultRenderer = patch.installed.getResultRenderer;
732
+ // 渲染上下文没有 result。ask_user_question 的标题要报问题数,只能从这里补。
733
+ patch.getRenderContext = function (this: any, ...args: any[]) {
734
+ const context = originalGetRenderContext.apply(this, args);
735
+ if (context && typeof context === "object") context.result = this.result;
736
+ return context;
737
+ };
738
+ prototype.getRenderContext = patch.getRenderContext;
621
739
  // fullscreen 滚动/选区每帧整树 render;内容未变时复用上一帧行,避免重做 diff/Box。
622
740
  patch.renderPaint = function (this: any, width: number): string[] {
623
741
  if (!patch.active || patch.mode() === "off") {
@@ -626,8 +744,11 @@ function installGlobalToolRendering(
626
744
  const cache = patch.paintCache;
627
745
  // A running partial tool owns a time-varying spinner. Light animation ticks
628
746
  // request a repaint without invalidating the component, so settled-content
629
- // caching must not intercept those paints.
630
- const cacheable = !(this.executionStarted && this.isPartial);
747
+ // caching must not intercept those paints. The waiting window before the
748
+ // tool is dispatched animates too, so it must bypass the cache as well.
749
+ const animatingRow =
750
+ this.isPartial && (this.executionStarted || isLiveToolCall(this.toolCallId));
751
+ const cacheable = !animatingRow;
631
752
  const hit = cacheable ? cache?.get(this) : undefined;
632
753
  if (hit && toolPaintMatches(hit, this, width)) return hit.lines;
633
754
  const lines = originalRender.call(this, width);
@@ -683,6 +804,13 @@ function deactivateGlobalToolRendering(patch: GlobalToolRenderPatch): void {
683
804
  ) {
684
805
  prototype.invalidate = patch.originalInvalidate;
685
806
  }
807
+ if (
808
+ patch.getRenderContext &&
809
+ prototype.getRenderContext === patch.getRenderContext &&
810
+ patch.originalGetRenderContext
811
+ ) {
812
+ prototype.getRenderContext = patch.originalGetRenderContext;
813
+ }
686
814
  }
687
815
 
688
816
  /** 展开面板背景统一为 user message 背景色;折叠行保持原生状态色。
@@ -31,8 +31,18 @@ import {
31
31
  TOOL_MOUSE_DISABLE,
32
32
  } from "./mouse/interaction.ts";
33
33
  import { getToolMouseTui, noteNewTranscriptItem } from "./mouse/scroll.ts";
34
+ import {
35
+ disposeStickyPromptPatches,
36
+ installStickyPromptPatches,
37
+ setStickyPromptTheme,
38
+ } from "./mouse/sticky-prompt.ts";
34
39
  import { setHoveredToolGroup, setHoveredToolIo } from "./mouse/hover.ts";
35
- import { clearAllAnimations } from "./tool/result.ts";
40
+ import {
41
+ clearAllAnimations,
42
+ clearLiveToolCalls,
43
+ noteStreamingToolCalls,
44
+ retireLiveToolCall,
45
+ } from "./tool/result.ts";
36
46
  import { installWriteOverride, WriteExecutionMetadataStore } from "./tool/diff/index.ts";
37
47
  import {
38
48
  installMessageDisplayRendering,
@@ -118,6 +128,8 @@ export default function (
118
128
  if (installation) return installation;
119
129
  // regular 主屏的差分渲染补丁:视口上方变化不再触发清回滚的 fullRender。
120
130
  installMainScreenDoRenderPatch();
131
+ // 粘性用户消息头:标记注入 + fullscreen 帧合成。
132
+ installStickyPromptPatches();
121
133
  const defaultMode = installDefaultMode(writeExecutionMetadata);
122
134
  const toolGrouping = installToolGrouping(() => config.mode === "on");
123
135
  const compactMode = installCompactMode({
@@ -184,6 +196,9 @@ export default function (
184
196
  });
185
197
 
186
198
  pi.on("message_update", async (event) => {
199
+ // 声明期(assistant 消息结束前)就记住工具 id:这段工具行已在等启动,
200
+ // spinner 该转,但 Pi 还没跑 markExecutionStarted。
201
+ if (event.message?.role === "assistant") noteStreamingToolCalls(event.message);
187
202
  // compact-thinking 在 session_start 时于 compact 补丁之上再装一层;
188
203
  // 扩展事件先于 interactive-mode 的 updateContent 派发,此处先重新认领。
189
204
  if (config.mode === "compact" && event.message?.role === "assistant") {
@@ -199,6 +214,7 @@ export default function (
199
214
  });
200
215
 
201
216
  pi.on("tool_execution_end", async (event) => {
217
+ retireLiveToolCall(event?.toolCallId);
202
218
  if (config.mode !== "compact") return;
203
219
  // Agent 等工具收尾后延迟刷新,让 compact-thinking 先落最终态。
204
220
  const toolCallId: string | undefined = event?.toolCallId;
@@ -227,6 +243,7 @@ export default function (
227
243
  if (!hooks) return;
228
244
  hooks.toolGrouping.setTheme(ctx.ui.theme);
229
245
  setMessageDisplayTheme(ctx.ui.theme);
246
+ setStickyPromptTheme(ctx.ui.theme);
230
247
  ctx.ui.setStatus("ccstyle", undefined);
231
248
  // 先收集 resume transcript,再同步 compact 补丁与全局展开状态。
232
249
  syncCompactMode(ctx);
@@ -244,6 +261,7 @@ export default function (
244
261
  if (!hooks) return;
245
262
  hooks.toolGrouping.setTheme(ctx.ui.theme);
246
263
  setMessageDisplayTheme(ctx.ui.theme);
264
+ setStickyPromptTheme(ctx.ui.theme);
247
265
  syncCompactMode(ctx);
248
266
  scheduleSessionRender(() => {
249
267
  syncCompactMode(ctx);
@@ -263,8 +281,18 @@ export default function (
263
281
  noteNewTranscriptItem();
264
282
  });
265
283
 
284
+ pi.on("turn_end", async () => {
285
+ clearLiveToolCalls();
286
+ });
287
+
288
+ pi.on("agent_end", async () => {
289
+ clearLiveToolCalls();
290
+ });
291
+
266
292
  pi.on("session_shutdown", async () => {
267
293
  writeExecutionMetadata.clear();
294
+ // 会话结束就清空,不能等 installation 早退判断;残留的 id 会让下一会话误转。
295
+ clearLiveToolCalls();
268
296
  // 鼠标交互独立于渲染层:fullscreen 下 installation 为 undefined,
269
297
  // 但 onTerminalInput 监听与 handleViewportInput 包装仍需释放。
270
298
  teardownToolMouseInteraction(mouseOwner);
@@ -276,6 +304,7 @@ export default function (
276
304
  current.compactMode.shutdown();
277
305
  compactModeHooks = undefined;
278
306
  current.disposeMessageDisplay();
307
+ disposeStickyPromptPatches();
279
308
  clearAllAnimations();
280
309
  installation = undefined;
281
310
  });
@@ -75,6 +75,11 @@ import {
75
75
  setHoveredToolIo,
76
76
  type FullscreenHoverTarget,
77
77
  } from "./hover.ts";
78
+ import {
79
+ handleStickyPromptClick,
80
+ resetStickyPromptState,
81
+ updateStickyPromptHover,
82
+ } from "./sticky-prompt.ts";
78
83
 
79
84
  type FrameToolRender = {
80
85
  component: any;
@@ -439,9 +444,15 @@ function handleFullscreenToolClick(tui: any, packet: SgrMousePacket): boolean {
439
444
  }
440
445
  // 展开 round 卡内工具时,让 compact 的强制折叠放行它(非 round 内工具为空操作)。
441
446
  if (isTool) markCompactRoundToolExpanded(component);
442
- // compact 摘要行自己记展开前的视口(收起时还原跟随),这里不动它。
443
- if (!isAssistant) keepViewportOnExpand(tui);
444
- component.setExpanded(true);
447
+ if (isAssistant) {
448
+ // compact 摘要行自己记展开前的视口(收起时还原跟随),所以它先展开再停跟随:
449
+ // 顺序反了快照会记下「未跟随」,收起时就交不回官方 follow。
450
+ component.setExpanded(true);
451
+ keepViewportOnExpand(tui);
452
+ } else {
453
+ keepViewportOnExpand(tui);
454
+ component.setExpanded(true);
455
+ }
445
456
  clearPendingCollapsePress();
446
457
  } else {
447
458
  // 普通工具截断头 show-more:打开全量预览(不收起)。
@@ -491,6 +502,7 @@ function isSameExpandedRound(other: any, owner: any): boolean {
491
502
  /**
492
503
  * 原地展开时停在当前滚动位置:transcript 跟随底部时,内容变高会把视口拖到底,
493
504
  * 点击的行和它上方的外层面板被顶出屏幕。展开前关掉跟随,点击行留在原位。
505
+ * compact 摘要行同样适用:它的展开入口在 setExpanded 之后调用本函数(见调用点)。
494
506
  */
495
507
  function keepViewportOnExpand(tui: any): void {
496
508
  const view = tui.getPrimaryScrollView?.();
@@ -611,12 +623,17 @@ function patchFullscreenViewportInput(tui: any): void {
611
623
  }
612
624
  } else if (!isSgrLeftPress(packet) && !isSgrIdleMotion(packet))
613
625
  clearPendingCollapsePressOnMove(packet);
626
+ if (isSgrLeftPress(packet) && handleStickyPromptClick(tui, packet)) {
627
+ return { consume: true };
628
+ }
614
629
  if (isSgrLeftPress(packet) && handleFullscreenToolClick(tui, packet)) {
615
630
  return { consume: true };
616
631
  }
617
632
  // 仅无按键移动走 hover;左键拖动(文本多选)放行官方选区,避免每像素命中+重绘。
618
633
  if (isSgrIdleMotion(packet)) {
619
- handleFullscreenToolHover(tui, packet);
634
+ // 粘性行命中时不进工具 hover:同一行只能有一个高亮。
635
+ if (updateStickyPromptHover(tui, packet)) applyFullscreenHover(tui, null);
636
+ else handleFullscreenToolHover(tui, packet);
620
637
  }
621
638
  }
622
639
  }
@@ -1021,6 +1038,7 @@ export function resetToolHoverState(): void {
1021
1038
  setHoveredCompactAssistant(null);
1022
1039
  setScrollButtonVisible(false);
1023
1040
  setScrollButtonHovered(false);
1041
+ resetStickyPromptState();
1024
1042
  restoreOfficialScrollToEnd(getToolMouseTui());
1025
1043
  releaseFullscreenToolMouseMotion(getToolMouseTui());
1026
1044
  }