@herbertgao/pi-extensions 2026.9.19 → 2026.9.20

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 (186) hide show
  1. package/README.md +1 -8
  2. package/node_modules/@herbertgao/pi-cc-extensions/README.en.md +76 -59
  3. package/node_modules/@herbertgao/pi-cc-extensions/README.md +76 -59
  4. package/node_modules/@herbertgao/pi-cc-extensions/assets/readme/preview.webp +0 -0
  5. package/node_modules/@herbertgao/pi-cc-extensions/extensions/config/config.ts +93 -34
  6. package/node_modules/@herbertgao/pi-cc-extensions/extensions/config/panel.ts +346 -117
  7. package/node_modules/@herbertgao/pi-cc-extensions/extensions/feature/agent-summary/core.ts +6 -36
  8. package/node_modules/@herbertgao/pi-cc-extensions/extensions/feature/agent-summary/index.ts +2 -2
  9. package/node_modules/@herbertgao/pi-cc-extensions/extensions/feature/compact-thinking.ts +24 -29
  10. package/node_modules/@herbertgao/pi-cc-extensions/extensions/feature/context.ts +330 -360
  11. package/node_modules/@herbertgao/pi-cc-extensions/extensions/feature/reference/index.ts +56 -108
  12. package/node_modules/@herbertgao/pi-cc-extensions/extensions/feature/reference/session.ts +97 -15
  13. package/node_modules/@herbertgao/pi-cc-extensions/extensions/feature/reference/subagent.ts +80 -45
  14. package/node_modules/@herbertgao/pi-cc-extensions/extensions/feature/shell/flush-docked-bash.ts +36 -0
  15. package/node_modules/@herbertgao/pi-cc-extensions/extensions/feature/shell/footer-layout.ts +229 -0
  16. package/node_modules/@herbertgao/pi-cc-extensions/extensions/feature/shell/footer.ts +479 -0
  17. package/node_modules/@herbertgao/pi-cc-extensions/extensions/feature/shell/startup-header.ts +92 -61
  18. package/node_modules/@herbertgao/pi-cc-extensions/extensions/feature/shell/working-message.ts +58 -38
  19. package/node_modules/@herbertgao/pi-cc-extensions/extensions/index.ts +19 -3
  20. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/compact-mode.ts +1080 -229
  21. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/default-mode.ts +232 -259
  22. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/index.ts +38 -48
  23. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/markdown-enhance.ts +8 -2
  24. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/mouse/hover.ts +2 -7
  25. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/mouse/interaction.ts +277 -113
  26. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/mouse/layout.ts +15 -6
  27. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/mouse/packets.ts +18 -72
  28. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/mouse/scroll.ts +61 -26
  29. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/tool/diff/ansi-utils.ts +25 -57
  30. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/tool/diff/diff-component.ts +125 -0
  31. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/tool/diff/diff-edit-render.ts +227 -0
  32. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/tool/diff/diff-header.ts +136 -0
  33. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/tool/diff/diff-highlight.ts +115 -0
  34. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/tool/diff/diff-inline.ts +324 -0
  35. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/tool/diff/diff-layout.ts +762 -0
  36. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/tool/diff/diff-limits.ts +152 -0
  37. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/tool/diff/diff-palette.ts +410 -0
  38. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/tool/diff/diff-parse.ts +535 -0
  39. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/tool/diff/diff-presentation.ts +2 -2
  40. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/tool/diff/diff-renderer.ts +21 -3219
  41. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/tool/diff/diff-text.ts +44 -0
  42. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/tool/diff/diff-write-render.ts +568 -0
  43. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/tool/diff/index.ts +19 -18
  44. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/tool/diff/line-width-safety.ts +2 -2
  45. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/tool/diff/write-execution.ts +1 -0
  46. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/tool/grouping.ts +142 -192
  47. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/tool/mcp-title.ts +39 -0
  48. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/{message-display.ts → tool/message-display.ts} +24 -90
  49. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/tool/names.ts +426 -0
  50. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/tool/result.ts +90 -132
  51. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/{show-more-hint.ts → tool/show-more-hint.ts} +10 -0
  52. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/transcript-refresh.ts +8 -26
  53. package/node_modules/@herbertgao/pi-cc-extensions/extensions/utils/ansi-color.ts +44 -0
  54. package/node_modules/@herbertgao/pi-cc-extensions/extensions/utils/ansi-text.ts +76 -0
  55. package/node_modules/@herbertgao/pi-cc-extensions/extensions/utils/component-tree.ts +34 -0
  56. package/node_modules/@herbertgao/pi-cc-extensions/extensions/utils/format.ts +28 -0
  57. package/node_modules/@herbertgao/pi-cc-extensions/extensions/utils/fullscreen-detect.ts +51 -0
  58. package/node_modules/@herbertgao/pi-cc-extensions/extensions/utils/patch-keys.ts +124 -0
  59. package/node_modules/@herbertgao/pi-cc-extensions/extensions/utils/sgr-mouse.ts +70 -0
  60. package/node_modules/@herbertgao/pi-cc-extensions/package.json +7 -7
  61. package/package.json +3 -7
  62. package/node_modules/@herbertgao/pi-cc-extensions/assets/readme/hero.en.svg +0 -101
  63. package/node_modules/@herbertgao/pi-cc-extensions/assets/readme/hero.svg +0 -101
  64. package/node_modules/@herbertgao/pi-cc-extensions/assets/readme/welcome.webp +0 -0
  65. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/tool/diff/render-utils.ts +0 -5
  66. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/tool/diff/types.ts +0 -30
  67. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/tool/diff/write-display-utils.ts +0 -12
  68. package/node_modules/pi-footer/CHANGELOG.md +0 -145
  69. package/node_modules/pi-footer/LICENSE +0 -21
  70. package/node_modules/pi-footer/README.md +0 -354
  71. package/node_modules/pi-footer/package.json +0 -68
  72. package/node_modules/pi-footer/src/cache.ts +0 -106
  73. package/node_modules/pi-footer/src/colors.ts +0 -237
  74. package/node_modules/pi-footer/src/config.ts +0 -202
  75. package/node_modules/pi-footer/src/event-widgets.ts +0 -41
  76. package/node_modules/pi-footer/src/extension-statuses.ts +0 -102
  77. package/node_modules/pi-footer/src/git.ts +0 -135
  78. package/node_modules/pi-footer/src/index.ts +0 -268
  79. package/node_modules/pi-footer/src/metrics.ts +0 -141
  80. package/node_modules/pi-footer/src/presets.ts +0 -383
  81. package/node_modules/pi-footer/src/render.ts +0 -114
  82. package/node_modules/pi-footer/src/separators.ts +0 -52
  83. package/node_modules/pi-footer/src/types.ts +0 -122
  84. package/node_modules/pi-footer/src/ui/color-level-confirm.ts +0 -22
  85. package/node_modules/pi-footer/src/ui/color-options.ts +0 -195
  86. package/node_modules/pi-footer/src/ui/config-lifecycle.ts +0 -61
  87. package/node_modules/pi-footer/src/ui/edit-colors.ts +0 -13
  88. package/node_modules/pi-footer/src/ui/events.ts +0 -35
  89. package/node_modules/pi-footer/src/ui/extension-status-picker.ts +0 -60
  90. package/node_modules/pi-footer/src/ui/extension-statuses.ts +0 -73
  91. package/node_modules/pi-footer/src/ui/fields.ts +0 -252
  92. package/node_modules/pi-footer/src/ui/global-menu.ts +0 -126
  93. package/node_modules/pi-footer/src/ui/helpers.ts +0 -44
  94. package/node_modules/pi-footer/src/ui/layout.ts +0 -17
  95. package/node_modules/pi-footer/src/ui/line-list.ts +0 -19
  96. package/node_modules/pi-footer/src/ui/model.ts +0 -64
  97. package/node_modules/pi-footer/src/ui/navigation.ts +0 -29
  98. package/node_modules/pi-footer/src/ui/option-edit.ts +0 -93
  99. package/node_modules/pi-footer/src/ui/overlay-render.ts +0 -103
  100. package/node_modules/pi-footer/src/ui/screen-context.ts +0 -18
  101. package/node_modules/pi-footer/src/ui/screen-controller.ts +0 -27
  102. package/node_modules/pi-footer/src/ui/screen-render.ts +0 -32
  103. package/node_modules/pi-footer/src/ui/screen-state.ts +0 -20
  104. package/node_modules/pi-footer/src/ui/screen.ts +0 -243
  105. package/node_modules/pi-footer/src/ui/screens/add-widget.ts +0 -92
  106. package/node_modules/pi-footer/src/ui/screens/confirm-exit.ts +0 -52
  107. package/node_modules/pi-footer/src/ui/screens/controller.ts +0 -12
  108. package/node_modules/pi-footer/src/ui/screens/edit-colors.ts +0 -68
  109. package/node_modules/pi-footer/src/ui/screens/edit-widget.ts +0 -115
  110. package/node_modules/pi-footer/src/ui/screens/extension-status-row.ts +0 -61
  111. package/node_modules/pi-footer/src/ui/screens/global.ts +0 -75
  112. package/node_modules/pi-footer/src/ui/screens/line-list.ts +0 -108
  113. package/node_modules/pi-footer/src/ui/screens/main.ts +0 -99
  114. package/node_modules/pi-footer/src/ui/screens/terminal.ts +0 -121
  115. package/node_modules/pi-footer/src/ui/screens/widget-list.ts +0 -154
  116. package/node_modules/pi-footer/src/ui/terminal-menu.ts +0 -32
  117. package/node_modules/pi-footer/src/ui/theme.ts +0 -48
  118. package/node_modules/pi-footer/src/ui/title-bar.ts +0 -100
  119. package/node_modules/pi-footer/src/ui/widget-actions.ts +0 -76
  120. package/node_modules/pi-footer/src/ui.ts +0 -66
  121. package/node_modules/pi-footer/src/widgets/context.ts +0 -28
  122. package/node_modules/pi-footer/src/widgets/core/active-tools.ts +0 -17
  123. package/node_modules/pi-footer/src/widgets/core/context-window.ts +0 -24
  124. package/node_modules/pi-footer/src/widgets/core/cwd-basename.ts +0 -19
  125. package/node_modules/pi-footer/src/widgets/core/cwd.ts +0 -108
  126. package/node_modules/pi-footer/src/widgets/core/event.ts +0 -34
  127. package/node_modules/pi-footer/src/widgets/core/external-status.ts +0 -102
  128. package/node_modules/pi-footer/src/widgets/core/model-provider.ts +0 -20
  129. package/node_modules/pi-footer/src/widgets/core/model.ts +0 -35
  130. package/node_modules/pi-footer/src/widgets/core/provider.ts +0 -17
  131. package/node_modules/pi-footer/src/widgets/core/session-name.ts +0 -17
  132. package/node_modules/pi-footer/src/widgets/core/text-verbosity.ts +0 -17
  133. package/node_modules/pi-footer/src/widgets/core/thinking-level.ts +0 -17
  134. package/node_modules/pi-footer/src/widgets/git/ahead-behind.ts +0 -17
  135. package/node_modules/pi-footer/src/widgets/git/branch.ts +0 -81
  136. package/node_modules/pi-footer/src/widgets/git/clean.ts +0 -19
  137. package/node_modules/pi-footer/src/widgets/git/deletions.ts +0 -17
  138. package/node_modules/pi-footer/src/widgets/git/diff.ts +0 -38
  139. package/node_modules/pi-footer/src/widgets/git/insertions.ts +0 -17
  140. package/node_modules/pi-footer/src/widgets/git/remote.ts +0 -17
  141. package/node_modules/pi-footer/src/widgets/git/root.ts +0 -17
  142. package/node_modules/pi-footer/src/widgets/git/sha.ts +0 -17
  143. package/node_modules/pi-footer/src/widgets/git/staged.ts +0 -17
  144. package/node_modules/pi-footer/src/widgets/git/status.ts +0 -19
  145. package/node_modules/pi-footer/src/widgets/git/unstaged.ts +0 -17
  146. package/node_modules/pi-footer/src/widgets/git/untracked.ts +0 -17
  147. package/node_modules/pi-footer/src/widgets/instance.ts +0 -118
  148. package/node_modules/pi-footer/src/widgets/layout/custom-text.ts +0 -18
  149. package/node_modules/pi-footer/src/widgets/layout/flex-separator.ts +0 -32
  150. package/node_modules/pi-footer/src/widgets/layout/separator.ts +0 -57
  151. package/node_modules/pi-footer/src/widgets/layout/spacer.ts +0 -31
  152. package/node_modules/pi-footer/src/widgets/options.ts +0 -122
  153. package/node_modules/pi-footer/src/widgets/project/runtime.ts +0 -361
  154. package/node_modules/pi-footer/src/widgets/registry.ts +0 -223
  155. package/node_modules/pi-footer/src/widgets/session/assistant-messages.ts +0 -17
  156. package/node_modules/pi-footer/src/widgets/session/compactions.ts +0 -17
  157. package/node_modules/pi-footer/src/widgets/session/elapsed.ts +0 -18
  158. package/node_modules/pi-footer/src/widgets/session/last-activity.ts +0 -18
  159. package/node_modules/pi-footer/src/widgets/session/messages.ts +0 -19
  160. package/node_modules/pi-footer/src/widgets/session/session-id.ts +0 -17
  161. package/node_modules/pi-footer/src/widgets/session/session-start.ts +0 -18
  162. package/node_modules/pi-footer/src/widgets/session/tool-results.ts +0 -17
  163. package/node_modules/pi-footer/src/widgets/session/total-messages.ts +0 -19
  164. package/node_modules/pi-footer/src/widgets/session/total-time.ts +0 -18
  165. package/node_modules/pi-footer/src/widgets/session/user-messages.ts +0 -17
  166. package/node_modules/pi-footer/src/widgets/store.ts +0 -26
  167. package/node_modules/pi-footer/src/widgets/tokens/cache-hit-rate.ts +0 -55
  168. package/node_modules/pi-footer/src/widgets/tokens/cache-read.ts +0 -18
  169. package/node_modules/pi-footer/src/widgets/tokens/cache-write.ts +0 -18
  170. package/node_modules/pi-footer/src/widgets/tokens/context-bar.ts +0 -108
  171. package/node_modules/pi-footer/src/widgets/tokens/context-length.ts +0 -26
  172. package/node_modules/pi-footer/src/widgets/tokens/context-remaining.ts +0 -29
  173. package/node_modules/pi-footer/src/widgets/tokens/context.ts +0 -29
  174. package/node_modules/pi-footer/src/widgets/tokens/cost.ts +0 -51
  175. package/node_modules/pi-footer/src/widgets/tokens/input-speed.ts +0 -25
  176. package/node_modules/pi-footer/src/widgets/tokens/input-tokens.ts +0 -18
  177. package/node_modules/pi-footer/src/widgets/tokens/output-speed.ts +0 -25
  178. package/node_modules/pi-footer/src/widgets/tokens/output-tokens.ts +0 -18
  179. package/node_modules/pi-footer/src/widgets/tokens/tokens.ts +0 -20
  180. package/node_modules/pi-footer/src/widgets/tokens/total-speed.ts +0 -25
  181. package/node_modules/pi-footer/src/widgets/tokens/total-tokens.ts +0 -18
  182. package/node_modules/pi-footer/src/widgets/types.ts +0 -181
  183. package/node_modules/pi-footer/src/widgets/utils/colors.ts +0 -11
  184. package/node_modules/pi-footer/src/widgets/utils/context.ts +0 -112
  185. package/node_modules/pi-footer/src/widgets/utils/session.ts +0 -19
  186. package/node_modules/pi-footer/src/widgets/utils/token-format.ts +0 -78
@@ -0,0 +1,36 @@
1
+ /**
2
+ * fullscreen 下 ! / !! 在 agent 仍 streaming 时挂到 dock(pendingMessages)。
3
+ * 官方只在下次普通 submit 才 flushPendingBashComponents,命令结束后卡钉在输入框上方。
4
+ * 在 handleBashCommand 收尾补一次 flush。
5
+ */
6
+ import { InteractiveMode } from "@earendil-works/pi-coding-agent";
7
+ import { FLUSH_DOCKED_BASH_PATCH, patchRegistry } from "../../utils/patch-keys.ts";
8
+
9
+ type Patch = {
10
+ active: boolean;
11
+ prototype: any;
12
+ original: (...args: any[]) => unknown;
13
+ installed: (...args: any[]) => unknown;
14
+ };
15
+
16
+ export function installFlushDockedBash(): void {
17
+ const prototype = InteractiveMode.prototype as any;
18
+ const previous = patchRegistry.get<Patch>(FLUSH_DOCKED_BASH_PATCH);
19
+ if (previous) previous.active = false;
20
+ const original =
21
+ previous && prototype.handleBashCommand === previous.installed
22
+ ? previous.original
23
+ : prototype.handleBashCommand;
24
+ const patch: Patch = {
25
+ active: true,
26
+ prototype,
27
+ original,
28
+ installed: async function (this: any, ...args: any[]) {
29
+ const result = await original.apply(this, args);
30
+ if (patch.active) this.flushPendingBashComponents();
31
+ return result;
32
+ },
33
+ };
34
+ prototype.handleBashCommand = patch.installed;
35
+ patchRegistry.install(FLUSH_DOCKED_BASH_PATCH, patch);
36
+ }
@@ -0,0 +1,229 @@
1
+ /**
2
+ * 自定义 footer 插件芯片布局:行、顺序、隐藏。
3
+ * 纯函数,供 config 规范化、footer 渲染、配置面板共用。
4
+ */
5
+
6
+ export const PI_USAGE_KEY = "pi-usage";
7
+ export const PI_USAGE_SOURCE = "@narumitw/pi-usage";
8
+ const LEGACY_PI_USAGE_KEYS = new Set(["localUsage", "piUsage"]);
9
+ export const SKIP_FOOTER_STATUS_KEY = "model";
10
+
11
+ export type FooterLineId = 1 | 2 | 3;
12
+
13
+ export type FooterChipLayout = {
14
+ footerHiddenKeys: string[];
15
+ footerLine1Keys: string[];
16
+ footerLine2Keys: string[];
17
+ footerLine3Keys: string[];
18
+ };
19
+
20
+ export const DEFAULT_FOOTER_CHIP_LAYOUT: FooterChipLayout = {
21
+ footerHiddenKeys: [],
22
+ footerLine1Keys: [PI_USAGE_KEY],
23
+ footerLine2Keys: [],
24
+ footerLine3Keys: [],
25
+ };
26
+
27
+ export function isSkippedFooterStatusKey(key: string): boolean {
28
+ return key === SKIP_FOOTER_STATUS_KEY;
29
+ }
30
+
31
+ /** 未单独配置时的默认行:usage 类和本包用量在 line1,其余 line2。不会默认进 line3。 */
32
+ export function defaultFooterLine(key: string): FooterLineId {
33
+ if (key === PI_USAGE_KEY) return 1;
34
+ if (/usage|quota|balance/i.test(key)) return 1;
35
+ return 2;
36
+ }
37
+
38
+ export function normalizeFooterKeyList(value: unknown): string[] {
39
+ if (!Array.isArray(value)) return [];
40
+ const out: string[] = [];
41
+ const seen = new Set<string>();
42
+ for (const item of value) {
43
+ if (typeof item !== "string") continue;
44
+ let key = item.trim();
45
+ if (LEGACY_PI_USAGE_KEYS.has(key)) key = PI_USAGE_KEY;
46
+ if (!key || isSkippedFooterStatusKey(key) || seen.has(key)) continue;
47
+ seen.add(key);
48
+ out.push(key);
49
+ }
50
+ return out;
51
+ }
52
+
53
+ function takeUnique(keys: string[], seen: Set<string>): string[] {
54
+ const out: string[] = [];
55
+ for (const key of keys) {
56
+ if (seen.has(key)) continue;
57
+ seen.add(key);
58
+ out.push(key);
59
+ }
60
+ return out;
61
+ }
62
+
63
+ /** 跨行去重(line1 → line2 → line3);补上 pi-usage 槽位。 */
64
+ export function normalizeFooterChipLayout(source: Record<string, unknown>): FooterChipLayout {
65
+ const hasHidden = Array.isArray(source.footerHiddenKeys);
66
+ const hasAnyLine = [source.footerLine1Keys, source.footerLine2Keys, source.footerLine3Keys].some(
67
+ Array.isArray,
68
+ );
69
+ const seen = new Set<string>();
70
+ let footerLine1Keys = takeUnique(normalizeFooterKeyList(source.footerLine1Keys), seen);
71
+ const footerLine2Keys = takeUnique(normalizeFooterKeyList(source.footerLine2Keys), seen);
72
+ const footerLine3Keys = takeUnique(normalizeFooterKeyList(source.footerLine3Keys), seen);
73
+ const footerHiddenKeys = normalizeFooterKeyList(source.footerHiddenKeys);
74
+
75
+ if (!seen.has(PI_USAGE_KEY)) {
76
+ footerLine1Keys = [PI_USAGE_KEY, ...footerLine1Keys];
77
+ }
78
+ if (!hasHidden && !hasAnyLine) {
79
+ return { ...DEFAULT_FOOTER_CHIP_LAYOUT };
80
+ }
81
+ return { footerHiddenKeys, footerLine1Keys, footerLine2Keys, footerLine3Keys };
82
+ }
83
+
84
+ /** 面板描述用:pi-usage 数据来自 @narumitw/pi-usage,其它芯片用来源 setStatus key。 */
85
+ export function footerChipSource(key: string): string {
86
+ return key === PI_USAGE_KEY ? PI_USAGE_SOURCE : key;
87
+ }
88
+
89
+ export function footerChipDescription(key: string, liveText: string): string {
90
+ const source = footerChipSource(key);
91
+ const live = liveText.trim();
92
+ if (live) return `${live} · from ${source}`;
93
+ if (key === PI_USAGE_KEY) return `From ${source}. No text right now.`;
94
+ return `inactive · from ${source}`;
95
+ }
96
+
97
+ export function footerLineOfKey(layout: FooterChipLayout, key: string): FooterLineId | undefined {
98
+ if (layout.footerLine1Keys.includes(key)) return 1;
99
+ if (layout.footerLine2Keys.includes(key)) return 2;
100
+ if (layout.footerLine3Keys.includes(key)) return 3;
101
+ return undefined;
102
+ }
103
+
104
+ function keysOnLine(layout: FooterChipLayout, line: FooterLineId): string[] {
105
+ if (line === 1) return layout.footerLine1Keys;
106
+ if (line === 2) return layout.footerLine2Keys;
107
+ return layout.footerLine3Keys;
108
+ }
109
+
110
+ function withLineKeys(
111
+ layout: FooterChipLayout,
112
+ line: FooterLineId,
113
+ keys: string[],
114
+ ): FooterChipLayout {
115
+ if (line === 1) return { ...layout, footerLine1Keys: keys };
116
+ if (line === 2) return { ...layout, footerLine2Keys: keys };
117
+ return { ...layout, footerLine3Keys: keys };
118
+ }
119
+
120
+ /**
121
+ * 配置里还没有的 live key 接到默认行末尾(按 key 字母序,避免每次渲染乱序)。
122
+ * pi-usage 由 normalize 保证已在某一行。
123
+ */
124
+ export function resolveFooterChipLayout(
125
+ layout: FooterChipLayout,
126
+ liveKeys: readonly string[],
127
+ ): FooterChipLayout {
128
+ const known = new Set([
129
+ ...layout.footerLine1Keys,
130
+ ...layout.footerLine2Keys,
131
+ ...layout.footerLine3Keys,
132
+ ]);
133
+ const extras = [
134
+ ...new Set(liveKeys.filter((key) => key && !isSkippedFooterStatusKey(key) && !known.has(key))),
135
+ ].sort((a, b) => a.localeCompare(b));
136
+ const next: FooterChipLayout = {
137
+ footerHiddenKeys: [...layout.footerHiddenKeys],
138
+ footerLine1Keys: [...layout.footerLine1Keys],
139
+ footerLine2Keys: [...layout.footerLine2Keys],
140
+ footerLine3Keys: [...layout.footerLine3Keys],
141
+ };
142
+ for (const key of extras) {
143
+ if (defaultFooterLine(key) === 1) next.footerLine1Keys.push(key);
144
+ else next.footerLine2Keys.push(key);
145
+ }
146
+ return next;
147
+ }
148
+
149
+ export function moveFooterKeyToLine(
150
+ layout: FooterChipLayout,
151
+ key: string,
152
+ line: FooterLineId,
153
+ ): FooterChipLayout {
154
+ if (isSkippedFooterStatusKey(key)) return layout;
155
+ const current = footerLineOfKey(layout, key);
156
+ if (current === line) return layout;
157
+ const stripped: FooterChipLayout = {
158
+ footerHiddenKeys: [...layout.footerHiddenKeys],
159
+ footerLine1Keys: layout.footerLine1Keys.filter((item) => item !== key),
160
+ footerLine2Keys: layout.footerLine2Keys.filter((item) => item !== key),
161
+ footerLine3Keys: layout.footerLine3Keys.filter((item) => item !== key),
162
+ };
163
+ return withLineKeys(stripped, line, [...keysOnLine(stripped, line), key]);
164
+ }
165
+
166
+ export function shiftFooterKeyLine(
167
+ layout: FooterChipLayout,
168
+ key: string,
169
+ delta: -1 | 1,
170
+ ): FooterChipLayout {
171
+ const current = footerLineOfKey(layout, key);
172
+ if (!current) return layout;
173
+ const next = (current + delta) as number;
174
+ if (next < 1 || next > 3) return layout;
175
+ return moveFooterKeyToLine(layout, key, next as FooterLineId);
176
+ }
177
+
178
+ export function reorderFooterKey(
179
+ layout: FooterChipLayout,
180
+ key: string,
181
+ delta: -1 | 1,
182
+ ): FooterChipLayout {
183
+ const line = footerLineOfKey(layout, key);
184
+ if (!line) return layout;
185
+ const keys = [...keysOnLine(layout, line)];
186
+ const index = keys.indexOf(key);
187
+ const nextIndex = index + delta;
188
+ if (index < 0 || nextIndex < 0 || nextIndex >= keys.length) return layout;
189
+ const swap = keys[nextIndex]!;
190
+ keys[nextIndex] = key;
191
+ keys[index] = swap;
192
+ return withLineKeys(layout, line, keys);
193
+ }
194
+
195
+ export function toggleFooterKeyHidden(layout: FooterChipLayout, key: string): FooterChipLayout {
196
+ if (isSkippedFooterStatusKey(key)) return layout;
197
+ const hidden = new Set(layout.footerHiddenKeys);
198
+ if (hidden.has(key)) hidden.delete(key);
199
+ else hidden.add(key);
200
+ return { ...layout, footerHiddenKeys: [...hidden] };
201
+ }
202
+
203
+ /** 按行顺序收集可见、且当前有文案的插件芯片文本。 */
204
+ export function visibleFooterPluginTexts(
205
+ lineKeys: readonly string[],
206
+ hiddenKeys: readonly string[],
207
+ texts: ReadonlyMap<string, string>,
208
+ ): string[] {
209
+ const hidden = new Set(hiddenKeys);
210
+ const out: string[] = [];
211
+ for (const key of lineKeys) {
212
+ if (hidden.has(key)) continue;
213
+ const text = texts
214
+ .get(key)
215
+ ?.replace(/[\r\n\t]+/g, " ")
216
+ .trim();
217
+ if (text) out.push(text);
218
+ }
219
+ return out;
220
+ }
221
+
222
+ export function formatFooterChipSummary(layout: FooterChipLayout): string {
223
+ const hidden = layout.footerHiddenKeys.length;
224
+ return `hidden=${hidden} · L1=${layout.footerLine1Keys.length} · L2=${layout.footerLine2Keys.length} · L3=${layout.footerLine3Keys.length}`;
225
+ }
226
+
227
+ export function orderedFooterKeys(layout: FooterChipLayout): string[] {
228
+ return [...layout.footerLine1Keys, ...layout.footerLine2Keys, ...layout.footerLine3Keys];
229
+ }