pi-one-ui 0.7.2 → 0.9.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.
Files changed (45) hide show
  1. package/CHANGELOG.md +52 -1
  2. package/README.en.md +5 -8
  3. package/README.md +6 -7
  4. package/extensions/app/config/footer.ts +21 -5
  5. package/extensions/app/config/renderer.ts +1 -12
  6. package/extensions/app/config/shell.ts +62 -60
  7. package/extensions/app/runtime/prototype-patch-registry.ts +13 -9
  8. package/extensions/app/runtime/tui-runtime.ts +19 -12
  9. package/extensions/app/settings/controller.ts +2 -2
  10. package/extensions/layouts/context/index.ts +2 -2
  11. package/extensions/layouts/context/message/user-message-osc.ts +1 -1
  12. package/extensions/layouts/context/message/user-message-styles.ts +9 -6
  13. package/extensions/layouts/context/message/user-message.ts +5 -5
  14. package/extensions/layouts/context/renderer/compact-mode.ts +2 -2
  15. package/extensions/layouts/context/renderer/default-mode.ts +10 -10
  16. package/extensions/layouts/context/renderer/index.ts +0 -1
  17. package/extensions/layouts/context/renderer/mouse/hover.ts +0 -4
  18. package/extensions/layouts/context/renderer/mouse/interaction.ts +106 -95
  19. package/extensions/layouts/context/renderer/mouse/layout.ts +0 -4
  20. package/extensions/layouts/context/renderer/mouse/scroll.ts +2 -183
  21. package/extensions/layouts/context/renderer/tool/diff/diff-edit-render.ts +1 -1
  22. package/extensions/layouts/context/renderer/tool/diff/index.ts +1 -1
  23. package/extensions/layouts/context/renderer/tool/grouping.ts +3 -3
  24. package/extensions/layouts/context/renderer/tool/message-display.ts +7 -7
  25. package/extensions/layouts/context/renderer/tool/result.ts +16 -16
  26. package/extensions/layouts/context/renderer/tool/toggle-render-cache.ts +9 -9
  27. package/extensions/layouts/editor/controller.ts +17 -17
  28. package/extensions/layouts/editor/editor-metadata-format.ts +5 -5
  29. package/extensions/layouts/editor/factory.ts +5 -5
  30. package/extensions/layouts/editor/minimalist-editor.ts +8 -8
  31. package/extensions/layouts/editor/ownership.ts +24 -20
  32. package/extensions/layouts/editor/ui.ts +10 -15
  33. package/extensions/layouts/footer/controller.ts +4 -4
  34. package/extensions/layouts/footer/data.ts +2 -2
  35. package/extensions/layouts/footer/extension-status.ts +4 -4
  36. package/extensions/layouts/footer/footer.ts +5 -2
  37. package/extensions/layouts/overlay/selector-border.ts +4 -4
  38. package/extensions/layouts/overlay/selector-controller.ts +1 -1
  39. package/extensions/layouts/overlay/settings-previews.ts +2 -2
  40. package/extensions/layouts/working-line/interaction-summary.ts +3 -1
  41. package/extensions/layouts/working-line/working-line.ts +4 -4
  42. package/extensions/services/package-data.ts +1 -1
  43. package/extensions/shared/style.ts +66 -56
  44. package/extensions/tools/patch-keys.ts +22 -25
  45. package/package.json +7 -7
@@ -1,4 +1,4 @@
1
- import { config } from "../../../../app/config/renderer.ts";
1
+ import { getKeybindings } from "@earendil-works/pi-tui";
2
2
  import {
3
3
  type InputRouter,
4
4
  inputRouter,
@@ -63,20 +63,9 @@ import {
63
63
  } from "./packets.ts";
64
64
  import {
65
65
  fullscreenLazyTui,
66
- getScrollButtonVisible,
67
- getScrollButtonWidget,
68
66
  getToolMouseTui,
69
- hideScrollButton,
70
- isScrollBottomInput,
71
- renderScrollButton,
72
- resetScrollButtonState,
73
- scheduleScrollButtonSync,
74
- setScrollButtonHovered,
75
- setScrollButtonVisible,
76
- setScrollButtonWidget,
77
67
  setToolMouseTui,
78
68
  toolMouseInteractionActive,
79
- updateScrollButtonFromInput,
80
69
  } from "./scroll.ts";
81
70
 
82
71
  type FrameToolRender = {
@@ -86,7 +75,7 @@ type FrameToolRender = {
86
75
  };
87
76
 
88
77
  type InteractionRegion = {
89
- kind: "collapsed-hint" | "expanded-card" | "show-more" | "scroll-bottom";
78
+ kind: "collapsed-hint" | "expanded-card" | "show-more";
90
79
  row: number;
91
80
  startCol: number;
92
81
  endCol: number;
@@ -97,10 +86,15 @@ type InteractionRegion = {
97
86
 
98
87
  type InteractionFrame = { regions: InteractionRegion[] };
99
88
 
100
- const TOOL_MOUSE_WIDGET_KEY = "ccstyle-tool-mouse";
89
+ const TOOL_MOUSE_WIDGET_KEY = "oneui-tool-mouse";
101
90
  const TOOL_MOUSE_MOTION_ENABLE = "\x1b[?1003h\x1b[?1006h";
102
91
  const TOOL_MOUSE_MOTION_DISABLE = "\x1b[?1003l";
103
- const FULLSCREEN_MOTION_ENABLED = Symbol("ccstyle.fullscreen-motion-enabled");
92
+ const FULLSCREEN_MOTION_ENABLED = Symbol("pi-one-ui.fullscreen-motion-enabled");
93
+ const FULLSCREEN_SCROLL_END_STYLE_PATCH = Symbol(
94
+ "pi-one-ui.fullscreen-scroll-end-style-patch",
95
+ );
96
+ /** 指示条 hover 状态:样式函数与命中检测同模块,模块级绑定即可。 */
97
+ let scrollEndIndicatorHovered = false;
104
98
  const DEFAULT_TOOL_MOUSE_OWNER = {};
105
99
  export const TOOL_MOUSE_DISABLE = "\x1b[?1006l\x1b[?1003l\x1b[?1000l";
106
100
 
@@ -118,7 +112,7 @@ let ownsFullscreenMotion = false;
118
112
  let sessionRenderTimer: ReturnType<typeof setTimeout> | null = null;
119
113
  let latestInteractionFrame: InteractionFrame = { regions: [] };
120
114
 
121
- /** Summary markers used by Pi and ccstyle; unlike the trailing hint, these survive truncation. */
115
+ /** Summary markers used by Pi and pi-one-ui; unlike the trailing hint, these survive truncation. */
122
116
  const COLLAPSED_TOOL_SUMMARY = /^\s*(?:↳|└|⎿|●|✓|✗|…)/;
123
117
 
124
118
  function interactionRegionAt(packet: SgrMousePacket): InteractionRegion | null {
@@ -130,7 +124,6 @@ function interactionRegionAt(packet: SgrMousePacket): InteractionRegion | null {
130
124
  );
131
125
  return (
132
126
  matches.find((region) => region.kind === "show-more") ??
133
- matches.find((region) => region.kind === "scroll-bottom") ??
134
127
  matches.find((region) => region.kind === "collapsed-hint") ??
135
128
  matches.find((region) => region.kind === "expanded-card") ??
136
129
  null
@@ -156,8 +149,6 @@ function tryOpenToolIoShowMore(region: InteractionRegion): boolean {
156
149
  function updateToolSummaryHover(tui: any, packet: SgrMousePacket): void {
157
150
  if ((packet.code & 32) === 0 || packet.final !== "M") return;
158
151
  const region = interactionRegionAt(packet);
159
- const nextScrollButtonHovered = region?.kind === "scroll-bottom";
160
- const scrollButtonChanged = setScrollButtonHovered(nextScrollButtonHovered);
161
152
  const component = region?.component;
162
153
  const nextToolCallId =
163
154
  region?.kind === "collapsed-hint" ? (component?.toolCallId ?? null) : null;
@@ -169,7 +160,6 @@ function updateToolSummaryHover(tui: any, packet: SgrMousePacket): void {
169
160
  const changed = nextToolCallId !== sharedToolHoverState().toolCallId;
170
161
  setHoveredToolCallId(nextToolCallId);
171
162
  if (
172
- scrollButtonChanged ||
173
163
  setHoveredToolIo(nextIoView, nextIoSection) ||
174
164
  setHoveredToolGroup(nextGroup) ||
175
165
  changed
@@ -218,7 +208,6 @@ function collapseExpandedCard(tui: any, card: any): boolean {
218
208
  function toggleToolAtMouseClick(tui: any, packet: SgrMousePacket): boolean {
219
209
  const region = interactionRegionAt(packet);
220
210
  if (!region) return false;
221
- if (region.kind === "scroll-bottom") return false;
222
211
  if (region.kind === "show-more") return tryOpenToolIoShowMore(region);
223
212
  const component = region.component;
224
213
  if (!component) return false;
@@ -255,13 +244,6 @@ function ensureFullscreenToolMouseMotion(tui: any): void {
255
244
  releaseFullscreenToolMouseMotion(tui);
256
245
  return;
257
246
  }
258
- // 面板改 scrollStepLines 后,下一帧渲染即同步(restore 仍按 original 恢复)。
259
- if (
260
- typeof tui.wheelScrollLines === "number" &&
261
- tui.wheelScrollLines !== config.scrollStepLines
262
- ) {
263
- tui.wheelScrollLines = config.scrollStepLines;
264
- }
265
247
  if (
266
248
  !toolMouseInteractionActive() ||
267
249
  tui.mouseEnabled === false ||
@@ -298,15 +280,12 @@ function releaseFullscreenToolMouseMotion(tui?: any): void {
298
280
  }
299
281
  }
300
282
 
301
- const FULLSCREEN_VIEWPORT_PATCH = Symbol("ccstyle.fullscreen-viewport-patch");
302
- const FULLSCREEN_WHEEL_SCROLL_ORIGINAL = Symbol(
303
- "ccstyle.fullscreen-wheel-scroll-original",
304
- );
283
+ const FULLSCREEN_VIEWPORT_PATCH = Symbol("pi-one-ui.fullscreen-viewport-patch");
305
284
 
306
285
  /**
307
286
  * 官方 fullscreen 工具卡点击:collapsed hint 单击展开
308
287
  * (有且仅保持一个展开:展开前收起其他工具卡),expanded 整卡双击收起,
309
- * 截断头 show-more 单击打开全量预览;回到底部按钮 scrollToBottom。
288
+ * 截断头 show-more 单击打开全量预览。
310
289
  * 滚动条列、含 OSC8 链接行、非工具区域、展开卡单击放行官方。
311
290
  */
312
291
  function handleFullscreenToolClick(tui: any, packet: SgrMousePacket): boolean {
@@ -316,6 +295,8 @@ function handleFullscreenToolClick(tui: any, packet: SgrMousePacket): boolean {
316
295
  const x = packet.col - 1;
317
296
  const y = packet.row - 1;
318
297
  if (isScrollbarColumnAt(layout, x)) return false;
298
+ // 指示条覆盖层优先:命中区放行官方,由官方 rect 命中触发 scrollToBottom。
299
+ if (isScrollEndIndicatorAt(tui, x, y)) return false;
319
300
  const hit = fullscreenLeafAt(layout, x, y);
320
301
  if (!hit) return false;
321
302
  const width = Math.max(1, Number(tui.terminal?.columns) || 80);
@@ -330,12 +311,6 @@ function handleFullscreenToolClick(tui: any, packet: SgrMousePacket): boolean {
330
311
  if (!target) return false;
331
312
  const component = target.component;
332
313
  const card = target.group ?? component;
333
- // 回到底部按钮:按组件引用命中,不依赖渲染行缓存。
334
- if (getScrollButtonVisible() && component === getScrollButtonWidget()) {
335
- tui.scrollToBottom?.();
336
- hideScrollButton(tui);
337
- return true;
338
- }
339
314
  const line = hit.box.lines?.[hit.localRow];
340
315
  if (typeof line !== "string" || /\x1b]8;[^;]*;/.test(line)) return false;
341
316
  const isTool = isToolExecutionComponent(component);
@@ -401,30 +376,96 @@ function handleFullscreenToolClick(tui: any, packet: SgrMousePacket): boolean {
401
376
 
402
377
  /**
403
378
  * fullscreen 鼠标悬停:collapsed 卡 [click to show more] hint、
404
- * expanded 卡截断头 show-more、回到底部按钮。motion 不 consume,官方链照常。
379
+ * expanded 卡截断头 show-more、回到底部指示条。motion 不 consume,官方链照常。
405
380
  */
381
+ function formatKeyDisplay(keys: string[]): string {
382
+ return keys
383
+ .join("/")
384
+ .split("/")
385
+ .map((part) =>
386
+ part
387
+ .split("+")
388
+ .map((piece) =>
389
+ piece.length <= 1
390
+ ? piece.toUpperCase()
391
+ : `${piece[0]?.toUpperCase() ?? ""}${piece.slice(1)}`,
392
+ )
393
+ .join("+"),
394
+ )
395
+ .join("/");
396
+ }
397
+
398
+ /** 回到底部快捷键(tui.altScreen.bottom)显示文本;未配置时省略快捷键段。 */
399
+ function scrollEndShortcutText(): string {
400
+ const keys = getKeybindings().getKeys("tui.altScreen.bottom");
401
+ return keys.length > 0 ? formatKeyDisplay(keys) : "";
402
+ }
403
+
404
+ /** pi-one-ui 按钮样式:[ ↓ Back to bottom · <key> ],accent 色、hover 转 text 色。 */
405
+ function renderScrollEndIndicatorLabel(theme: any): string {
406
+ const shortcut = scrollEndShortcutText();
407
+ const label = `[ ↓ Back to bottom${shortcut ? ` · ${shortcut}` : ""} ]`;
408
+ return theme.fg(scrollEndIndicatorHovered ? "text" : "accent", label);
409
+ }
410
+
411
+ /** 指示条是 composite 层覆盖物(不在布局树内),按官方 rect 判定命中。 */
412
+ function isScrollEndIndicatorAt(tui: any, x: number, y: number): boolean {
413
+ const rect = tui.scrollToEndIndicatorRect;
414
+ return Boolean(
415
+ rect && y === rect.row && x >= rect.column && x < rect.column + rect.width,
416
+ );
417
+ }
418
+
419
+ /** 指示条 hover 同步;返回是否悬停在指示条上(命中时优先于下方组件)。 */
420
+ function updateScrollEndIndicatorHover(
421
+ tui: any,
422
+ x: number,
423
+ y: number,
424
+ ): boolean {
425
+ const hovered = isScrollEndIndicatorAt(tui, x, y);
426
+ if (hovered !== scrollEndIndicatorHovered) {
427
+ scrollEndIndicatorHovered = hovered;
428
+ tui.requestRender?.();
429
+ }
430
+ return hovered;
431
+ }
432
+
433
+ /**
434
+ * 覆盖官方 scrollToEndIndicator 样式函数(实例字段,官方每帧调用取标签文本;
435
+ * 显示条件、居中、点击命中、scrollToBottom 均保留官方机制)。惰性 Proxy 的
436
+ * get 会包装函数属性,保存到 Symbol 的是等价转发包装,恢复后行为不变。
437
+ */
438
+ function installScrollEndIndicatorStyle(tui: any, theme: any): void {
439
+ if (tui[FULLSCREEN_SCROLL_END_STYLE_PATCH]) return;
440
+ const original = tui.scrollToEndIndicator;
441
+ if (typeof original !== "function") return;
442
+ tui[FULLSCREEN_SCROLL_END_STYLE_PATCH] = original;
443
+ tui.scrollToEndIndicator = () => renderScrollEndIndicatorLabel(theme);
444
+ }
445
+
446
+ function restoreScrollEndIndicatorStyle(tui: any): void {
447
+ const original = tui?.[FULLSCREEN_SCROLL_END_STYLE_PATCH];
448
+ if (typeof original !== "function") return;
449
+ tui.scrollToEndIndicator = original;
450
+ tui[FULLSCREEN_SCROLL_END_STYLE_PATCH] = undefined;
451
+ scrollEndIndicatorHovered = false;
452
+ }
453
+
406
454
  function handleFullscreenToolHover(tui: any, packet: SgrMousePacket): void {
407
455
  if (packet.final !== "M") return;
408
456
  const layout = tui.currentLayout;
409
457
  if (!layout?.root) return;
410
458
  const x = packet.col - 1;
411
459
  const y = packet.row - 1;
460
+ if (updateScrollEndIndicatorHover(tui, x, y)) {
461
+ applyFullscreenHover(tui, null);
462
+ return;
463
+ }
412
464
  let target: FullscreenHoverTarget | null = null;
413
465
  const hit = fullscreenLeafAt(layout, x, y);
414
466
  if (hit) {
415
467
  const line = hit.box.lines?.[hit.localRow];
416
- // 回到底部按钮:渲染行文本 + 列区间识别(零组件树开销)。
417
- if (
418
- typeof line === "string" &&
419
- getScrollButtonVisible() &&
420
- line.includes("[ ↓")
421
- ) {
422
- const plain = stripTerminalSequencesPreservingLayout(line);
423
- const idx = plain.indexOf("[ ↓");
424
- if (idx >= 0 && x >= idx && x <= idx + plain.length - 1) {
425
- target = { kind: "button" };
426
- }
427
- } else if (typeof line === "string" && !/\x1b]8;/.test(line)) {
468
+ if (typeof line === "string" && !/\x1b]8;/.test(line)) {
428
469
  const width = Math.max(1, Number(tui.terminal?.columns) || 80);
429
470
  const contentWidth = fullscreenContentWidth(hit.box, width);
430
471
  // 与点击共用同一定位算法,避免 hover 自建行段与真实组件树错位。
@@ -489,17 +530,9 @@ function patchFullscreenViewportInput(tui: any): void {
489
530
  const proto = Object.getPrototypeOf(tui);
490
531
  const original = proto?.handleViewportInput;
491
532
  if (typeof original !== "function") return;
492
- // 官方原生 routeWheel 已完整处理嵌套 ScrollView;只调整默认步进(config.scrollStepLines)。
493
- if (typeof tui.wheelScrollLines === "number") {
494
- tui[FULLSCREEN_WHEEL_SCROLL_ORIGINAL] = tui.wheelScrollLines;
495
- tui.wheelScrollLines = config.scrollStepLines;
496
- }
497
533
  tui[FULLSCREEN_VIEWPORT_PATCH] = true;
498
534
  tui.handleViewportInput = function (this: any, data: string) {
499
535
  if (toolMouseInteractionActive() && tui.mode === "fullscreen") {
500
- // 滚动输入(wheel/pageUp/end 等)后同步回到底部按钮显隐;
501
- // 官方 viewport 会消费键盘,扩展监听器无法补偿,必须在这里调度。
502
- scheduleScrollButtonSync(tui, data);
503
536
  const packets = parseSgrMousePackets(data);
504
537
  // 官方 fullscreen 会消费全部鼠标;文本预览 overlay 活动时放行给 focused
505
538
  // custom component,使 [esc] 点击和滚轮可用。
@@ -529,11 +562,6 @@ function restoreFullscreenViewportInput(tui: any): void {
529
562
  if (typeof proto?.handleViewportInput === "function") {
530
563
  tui.handleViewportInput = proto.handleViewportInput;
531
564
  }
532
- const originalWheelScrollLines = tui[FULLSCREEN_WHEEL_SCROLL_ORIGINAL];
533
- if (typeof originalWheelScrollLines === "number") {
534
- tui.wheelScrollLines = originalWheelScrollLines;
535
- tui[FULLSCREEN_WHEEL_SCROLL_ORIGINAL] = undefined;
536
- }
537
565
  tui[FULLSCREEN_VIEWPORT_PATCH] = false;
538
566
  }
539
567
 
@@ -857,24 +885,12 @@ function handleToolMouseInput(data: string): { consume: true } | undefined {
857
885
  if (!getToolMouseTui()) return undefined;
858
886
  // 惰性 Proxy fullscreen:鼠标由 handleViewportInput 包装消费(官方链之前),
859
887
  // 此处只处理键盘(鼠标事件在官方 listener 已被 consume,到不了这里)。
860
- if (fullscreenLazyTui(getToolMouseTui())) {
861
- scheduleScrollButtonSync(getToolMouseTui(), data);
862
- if (isScrollBottomInput(data)) {
863
- getToolMouseTui().scrollToBottom?.();
864
- hideScrollButton(getToolMouseTui());
865
- return { consume: true };
866
- }
867
- return undefined;
868
- }
869
- updateScrollButtonFromInput(getToolMouseTui(), data);
888
+ if (fullscreenLazyTui(getToolMouseTui())) return undefined;
870
889
  // Off mode restores native input: wheel keeps scrolling through Pi's normal
871
890
  // dispatcher, while hover/click affordances are entirely inactive.
872
891
  if (!toolMouseInteractionActive()) return undefined;
873
892
  const packets = parseSgrMousePackets(data);
874
- if (!packets) {
875
- scheduleScrollButtonSync(getToolMouseTui(), data);
876
- return undefined;
877
- }
893
+ if (!packets) return undefined;
878
894
 
879
895
  let consumed = false;
880
896
  for (const packet of packets) {
@@ -887,7 +903,6 @@ function handleToolMouseInput(data: string): { consume: true } | undefined {
887
903
 
888
904
  // Let scrolling, motion, release, and clicks outside tool results reach the
889
905
  // normal TUI input chain (including other extensions such as pi-zentui).
890
- scheduleScrollButtonSync(getToolMouseTui(), data);
891
906
  return consumed ? { consume: true } : undefined;
892
907
  }
893
908
 
@@ -923,21 +938,19 @@ export function teardownToolMouseInteraction(
923
938
  }
924
939
  restoreToolMouseRenderPatch();
925
940
  restoreFullscreenViewportInput(getToolMouseTui());
926
- resetScrollButtonState();
941
+ restoreScrollEndIndicatorStyle(getToolMouseTui());
927
942
  setToolMouseTui(null);
928
943
  toolMouseUi = null;
929
944
  patchRegistry.dispose(TOOL_MOUSE_OWNER_KEY, owner);
930
945
  if (toolMouseInstallationOwner === owner) toolMouseInstallationOwner = null;
931
946
  }
932
947
 
933
- /** off 模式清理:清空 hover 与回到底部按钮状态(跨模块 rebind 统一经由此函数)。 */
948
+ /** off 模式清理:清空 hover 状态(跨模块 rebind 统一经由此函数)。 */
934
949
  export function resetToolHoverState(): void {
935
950
  setHoveredToolCallId(null);
936
951
  setHoveredThinking(null);
937
952
  setHoveredMessageDisplay(null);
938
953
  setHoveredCompactAssistant(null);
939
- setScrollButtonVisible(false);
940
- setScrollButtonHovered(false);
941
954
  releaseFullscreenToolMouseMotion(getToolMouseTui());
942
955
  }
943
956
 
@@ -983,26 +996,24 @@ export function installToolMouseInteraction(
983
996
  if (isLazyProxyTui(tui)) {
984
997
  patchFullscreenViewportInput(tui);
985
998
  ensureFullscreenToolMouseMotion(tui);
986
- setScrollButtonWidget({
987
- render: (width: number) => {
999
+ installScrollEndIndicatorStyle(tui, theme);
1000
+ return {
1001
+ render: () => {
1002
+ // 惰性 Proxy 下 renderer 可能被替换,每帧补装 viewport 包装与 motion。
988
1003
  patchFullscreenViewportInput(tui);
989
1004
  ensureFullscreenToolMouseMotion(tui);
990
- return renderScrollButton(width, theme);
1005
+ installScrollEndIndicatorStyle(tui, theme);
1006
+ return [];
991
1007
  },
992
1008
  invalidate() {},
993
- });
994
- return getScrollButtonWidget();
1009
+ };
995
1010
  }
996
1011
  // Wrap doRender to capture the live frame for tool click/hover mapping.
997
1012
  patchToolMouseMotionAfterRender(tui);
1013
+ installScrollEndIndicatorStyle(tui, theme);
998
1014
  if (toolMouseInteractionActive())
999
1015
  tui?.terminal?.write?.(TOOL_MOUSE_MOTION_ENABLE);
1000
- const widget = {
1001
- render: (width: number) => renderScrollButton(width, theme),
1002
- invalidate() {},
1003
- };
1004
- setScrollButtonWidget(widget);
1005
- return widget;
1016
+ return { render: () => [], invalidate() {} };
1006
1017
  });
1007
1018
  const removeInput = services.input.bind(
1008
1019
  ctx.ui,
@@ -7,7 +7,6 @@ import {
7
7
  isToolExecutionComponent,
8
8
  stripTerminalSequencesPreservingLayout,
9
9
  } from "./packets.ts";
10
- import { getScrollButtonWidget } from "./scroll.ts";
11
10
 
12
11
  export type ComponentRowHit = {
13
12
  component: any;
@@ -156,9 +155,6 @@ export function componentAtLocalRow(
156
155
  if (isMessageDisplayComponent(component)) {
157
156
  return { component, row: localRow };
158
157
  }
159
- if (component === getScrollButtonWidget()) {
160
- return { component, row: localRow };
161
- }
162
158
  if (!Array.isArray(component.children)) return null;
163
159
  let offset = 0;
164
160
  for (const child of component.children) {
@@ -1,28 +1,13 @@
1
- import {
2
- getKeybindings,
3
- type Keybinding,
4
- matchesKey,
5
- truncateToWidth,
6
- visibleWidth,
7
- } from "@earendil-works/pi-tui";
8
1
  import { config } from "../../../../app/config/renderer.ts";
9
2
  import { isLazyProxyTui } from "../../../../tools/fullscreen-detect.ts";
10
3
  import {
11
4
  patchRegistry,
12
- SCROLL_BUTTON_STATE_SLOT,
13
5
  TOOL_MOUSE_TUI_SLOT,
14
6
  } from "../../../../tools/patch-keys.ts";
15
- import { parseSgrMousePackets } from "./packets.ts";
16
-
17
- const ZENTUI_PAGE_UP_INPUT =
18
- /^\x1b\[5;9(?::[12])?~$|^\x1b\[57421;9(?::[12])?u$|^\x1b\[1;6A$/;
19
- const ZENTUI_PAGE_DOWN_INPUT =
20
- /^\x1b\[6;9(?::[12])?~$|^\x1b\[57422;9(?::[12])?u$|^\x1b\[1;6B$/;
21
- const SCROLL_BOTTOM_SHORTCUT = "ctrl+end";
22
7
 
23
8
  /**
24
- * 当前安装的 tui 宿主。宿主放本模块(滚动按钮/调度依赖它),
25
- * 由 mouse-interaction 经 setToolMouseTui 维护;跨模块一律经绑定/setter 访问。
9
+ * 当前安装的 tui 宿主。由 mouse-interaction 经 setToolMouseTui 维护;
10
+ * 跨模块一律经绑定/setter 访问。
26
11
  *
27
12
  * 状态镜像到 globalThis(Symbol 槽):jiti 转译下经 re-export 链读取的
28
13
  * 模块级 let 绑定是初始值快照(死绑定,实测恒 null),函数调用才是活引用。
@@ -38,50 +23,6 @@ export function setToolMouseTui(tui: any): void {
38
23
  patchRegistry.install(TOOL_MOUSE_TUI_SLOT, tui);
39
24
  }
40
25
 
41
- // 滚动按钮状态同 toolMouseTui:镜像到 globalThis(Symbol 槽),跨模块读取
42
- // 一律用 getter(jiti 转译下模块级 let 绑定是初始值快照)。
43
- type ScrollButtonState = { visible: boolean; hovered: boolean; widget: any };
44
- function scrollButtonState(): ScrollButtonState {
45
- return patchRegistry.ensure(SCROLL_BUTTON_STATE_SLOT, () => ({
46
- visible: false,
47
- hovered: false,
48
- widget: null,
49
- }));
50
- }
51
- export function getScrollButtonVisible(): boolean {
52
- return scrollButtonState().visible;
53
- }
54
- export function getScrollButtonHovered(): boolean {
55
- return scrollButtonState().hovered;
56
- }
57
- export function getScrollButtonWidget(): any {
58
- return scrollButtonState().widget;
59
- }
60
- export function setScrollButtonVisible(visible: boolean): void {
61
- scrollButtonState().visible = visible;
62
- }
63
-
64
- /** 返回是否发生变化(调用方据此决定是否需要重渲染)。 */
65
- export function setScrollButtonHovered(hovered: boolean): boolean {
66
- if (hovered === scrollButtonState().hovered) return false;
67
- scrollButtonState().hovered = hovered;
68
- return true;
69
- }
70
-
71
- export function setScrollButtonWidget(widget: any): void {
72
- scrollButtonState().widget = widget;
73
- }
74
-
75
- /** teardown 全量清零(visible/hovered/widget/sync 调度)。 */
76
- export function resetScrollButtonState(): void {
77
- scrollButtonState().visible = false;
78
- scrollButtonState().hovered = false;
79
- scrollButtonState().widget = null;
80
- scrollButtonSyncScheduled = false;
81
- }
82
-
83
- let scrollButtonSyncScheduled = false;
84
-
85
26
  // 交互开关只取决于配置模式:原实现按 isLazyProxyTui(toolMouseTui) 分两分支,
86
27
  // 两分支恒真(0.84+ 惰性 Proxy 下判定不再影响开关),折叠为单条件。
87
28
  export function toolMouseInteractionActive(): boolean {
@@ -92,125 +33,3 @@ export function toolMouseInteractionActive(): boolean {
92
33
  export function fullscreenLazyTui(tui: any): boolean {
93
34
  return isLazyProxyTui(tui) && tui.mode === "fullscreen";
94
35
  }
95
-
96
- /**
97
- * Reports whether fullscreen output is following the bottom of the context.
98
- */
99
- export function isFullscreenAtBottom(tui: any): boolean {
100
- const following =
101
- tui.isFollowingOutput ??
102
- tui.getPrimaryScrollView?.()?.isFollowingEnd ??
103
- true;
104
- return Boolean(following);
105
- }
106
-
107
- function formatShortcut(shortcut: string): string {
108
- return shortcut
109
- .split("+")
110
- .map((part) =>
111
- part.length <= 1
112
- ? part.toUpperCase()
113
- : `${part[0]?.toUpperCase() ?? ""}${part.slice(1)}`,
114
- )
115
- .join("+");
116
- }
117
-
118
- export function isScrollBottomInput(data: string): boolean {
119
- return matchesKey(data, SCROLL_BOTTOM_SHORTCUT);
120
- }
121
-
122
- function isScrollNavigationInput(data: string): boolean {
123
- if (
124
- matchesKey(data, "pageUp") ||
125
- matchesKey(data, "pageDown") ||
126
- ZENTUI_PAGE_UP_INPUT.test(data) ||
127
- ZENTUI_PAGE_DOWN_INPUT.test(data) ||
128
- // 官方 fullscreen viewport 的可滚动键(half-page/prompt/top/bottom)。
129
- [
130
- "tui.altScreen.pageUp",
131
- "tui.altScreen.pageDown",
132
- "tui.altScreen.halfPageUp",
133
- "tui.altScreen.halfPageDown",
134
- "tui.altScreen.previousPrompt",
135
- "tui.altScreen.nextPrompt",
136
- "tui.altScreen.top",
137
- "tui.altScreen.bottom",
138
- ].some((key) => getKeybindings().matches(data, key as Keybinding))
139
- ) {
140
- return true;
141
- }
142
- const packets = parseSgrMousePackets(data);
143
- return Boolean(
144
- packets?.some((packet) => {
145
- const baseButton = packet.code & ~(4 | 8 | 16 | 32);
146
- return packet.final === "M" && (baseButton === 64 || baseButton === 65);
147
- }),
148
- );
149
- }
150
-
151
- /**
152
- * Reports whether the context is at the bottom for the detected TUI mode.
153
- */
154
- function isAtContextBottom(tui: any): boolean {
155
- // 惰性 Proxy fullscreen:官方 viewport 以 isFollowingOutput 判定是否在底部。
156
- if (fullscreenLazyTui(tui)) return isFullscreenAtBottom(tui);
157
- return true;
158
- }
159
-
160
- export function hideScrollButton(tui: any): void {
161
- const changed = getScrollButtonVisible() || getScrollButtonHovered();
162
- setScrollButtonVisible(false);
163
- setScrollButtonHovered(false);
164
- if (changed) tui.requestRender?.();
165
- }
166
-
167
- export function scheduleScrollButtonSync(tui: any, data: string): void {
168
- if (
169
- !fullscreenLazyTui(tui) ||
170
- !toolMouseInteractionActive() ||
171
- !isScrollNavigationInput(data) ||
172
- scrollButtonSyncScheduled
173
- )
174
- return;
175
- scrollButtonSyncScheduled = true;
176
- const previousLines = tui.previousLines;
177
- const check = (attempt: number) => {
178
- scrollButtonSyncScheduled = false;
179
- if (toolMouseTui !== tui) return;
180
- // Pi renders on its own frame timer. Inspect the resulting viewport before
181
- // showing the button so empty or non-scrollable contexts never flash it.
182
- const rendered = tui.previousLines !== previousLines;
183
- // fullscreen 下 isFollowingOutput 是即时状态,无需等待官方帧渲染。
184
- if (!rendered && attempt < 4 && !fullscreenLazyTui(tui)) {
185
- scrollButtonSyncScheduled = true;
186
- const timer = setTimeout(() => check(attempt + 1), 16);
187
- if (typeof timer === "object" && timer !== null && "unref" in timer) {
188
- (timer as { unref: () => void }).unref();
189
- }
190
- return;
191
- }
192
- const nextVisible = !isAtContextBottom(tui);
193
- if (nextVisible !== getScrollButtonVisible()) {
194
- setScrollButtonVisible(nextVisible);
195
- tui.requestRender?.();
196
- }
197
- };
198
- process.nextTick(() => check(0));
199
- }
200
-
201
- export function updateScrollButtonFromInput(tui: any, data: string): void {
202
- if (!fullscreenLazyTui(tui) || !toolMouseInteractionActive()) return;
203
- if (matchesKey(data, "enter") || matchesKey(data, "return"))
204
- hideScrollButton(tui);
205
- }
206
-
207
- export function renderScrollButton(width: number, theme: any): string[] {
208
- if (!getScrollButtonVisible() || !fullscreenLazyTui(toolMouseTui)) return [];
209
- const shortcut = formatShortcut(SCROLL_BOTTOM_SHORTCUT);
210
- const label = theme.fg(
211
- getScrollButtonHovered() ? "text" : "accent",
212
- `[ ↓ Back to bottom · ${shortcut} ]`,
213
- );
214
- const leftPad = Math.max(0, Math.floor((width - visibleWidth(label)) / 2));
215
- return [`${" ".repeat(leftPad)}${truncateToWidth(label, width, "…")}`];
216
- }
@@ -119,7 +119,7 @@ export function renderEditDiffResult(
119
119
  showHashlineAnchors,
120
120
  );
121
121
  const palette = resolveDiffPalette(theme);
122
- // Rich diffs use ccstyle's self shell. Keep the panel transparent so the
122
+ // Rich diffs use pi-one-ui's self shell. Keep the panel transparent so the
123
123
  // separator cannot leak toolSuccessBg across the entire new column.
124
124
  const containerBgAnsi = undefined;
125
125
  const language = resolveLanguageFromPath(options.filePath);
@@ -46,7 +46,7 @@ export function renderRichToolResult(
46
46
  theme: any,
47
47
  context: any,
48
48
  writeMetadata: WriteExecutionMetadataStore,
49
- /** Plain snapshot or live getter — getter lets /ccstyle panel changes repaint existing diffs. */
49
+ /** Plain snapshot or live getter — getter lets /oneui panel changes repaint existing diffs. */
50
50
  displayConfig: DisplayConfigInput = DEFAULT_TOOL_DISPLAY_CONFIG,
51
51
  ): any | undefined {
52
52
  if (options?.isPartial || options?.isError || context?.isError)
@@ -213,8 +213,8 @@ type SettledExpandedChildCache = {
213
213
 
214
214
  function toolIoView(tool: any): ExpandedToolIoView | undefined {
215
215
  for (const candidate of [
216
- tool?.rendererState?.ccstyleIoView,
217
- tool?.state?.ccstyleIoView,
216
+ tool?.rendererState?.oneuiIoView,
217
+ tool?.state?.oneuiIoView,
218
218
  tool?.resultRendererComponent,
219
219
  ]) {
220
220
  if (isExpandedToolIoView(candidate)) {
@@ -237,7 +237,7 @@ function ioRenderRevision(
237
237
  }
238
238
 
239
239
  export class ToolGroupComponent extends Container {
240
- readonly toolCallId = `ccstyle-tool-group-${nextGroupId++}`;
240
+ readonly toolCallId = `oneui-tool-group-${nextGroupId++}`;
241
241
  readonly toolName = "Tool group";
242
242
  private _expanded = false;
243
243
  /** 分组是否展开(只读;测试与外部读状态用)。 */