pi-one-ui 0.1.1 → 0.2.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 (113) hide show
  1. package/README.md +66 -39
  2. package/extensions/{shell → app/commands}/settings-previews.ts +7 -7
  3. package/extensions/app/config/renderer.ts +13 -63
  4. package/extensions/app/config/shell.ts +24 -138
  5. package/extensions/app/config/store.ts +290 -0
  6. package/extensions/app/host/pi-extension-port.ts +25 -0
  7. package/extensions/app/host/pi-ui-port.ts +52 -0
  8. package/extensions/app/host/tui-capabilities.ts +27 -0
  9. package/extensions/app/overlay/input-router.ts +63 -0
  10. package/extensions/app/overlay/overlay-manager.ts +39 -0
  11. package/extensions/{shell → app/overlay}/selector-border.ts +3 -3
  12. package/extensions/app/overlay/selector-controller.ts +104 -0
  13. package/extensions/app/ownership/layout-registry.ts +65 -0
  14. package/extensions/app/ownership.ts +21 -7
  15. package/extensions/app/panel.ts +261 -140
  16. package/extensions/app/runtime/event-coordinator.ts +84 -0
  17. package/extensions/app/runtime/render-scheduler.ts +56 -0
  18. package/extensions/app/runtime/runtime-state.ts +83 -0
  19. package/extensions/app/runtime/tui-runtime.ts +546 -0
  20. package/extensions/features/{context.ts → context-inspector/index.ts} +30 -23
  21. package/extensions/features/{shell/flush-docked-bash.ts → flush-docked-bash.ts} +1 -4
  22. package/extensions/index.ts +2 -48
  23. package/extensions/layouts/context/index.ts +188 -0
  24. package/extensions/{shell → layouts/context/message}/user-message-styles.ts +2 -2
  25. package/extensions/{shell → layouts/context/message}/user-message.ts +2 -2
  26. package/extensions/{transcript → layouts/context}/renderer/compact-mode.ts +17 -13
  27. package/extensions/layouts/context/renderer/context-refresh.ts +79 -0
  28. package/extensions/{transcript → layouts/context}/renderer/default-mode.ts +3 -3
  29. package/extensions/{transcript → layouts/context}/renderer/index.ts +14 -14
  30. package/extensions/{transcript → layouts/context}/renderer/mouse/hover.ts +2 -2
  31. package/extensions/{transcript → layouts/context}/renderer/mouse/interaction.ts +26 -7
  32. package/extensions/{transcript → layouts/context}/renderer/mouse/layout.ts +1 -1
  33. package/extensions/{transcript → layouts/context}/renderer/mouse/packets.ts +3 -3
  34. package/extensions/{transcript → layouts/context}/renderer/mouse/scroll.ts +12 -7
  35. package/extensions/{transcript → layouts/context}/renderer/tool/diff/diff-component.ts +2 -2
  36. package/extensions/{transcript → layouts/context}/renderer/tool/diff/diff-edit-render.ts +1 -1
  37. package/extensions/{transcript → layouts/context}/renderer/tool/diff/diff-highlight.ts +1 -1
  38. package/extensions/{transcript → layouts/context}/renderer/tool/diff/diff-layout.ts +1 -1
  39. package/extensions/{transcript → layouts/context}/renderer/tool/diff/diff-limits.ts +1 -1
  40. package/extensions/{transcript → layouts/context}/renderer/tool/diff/diff-palette.ts +1 -1
  41. package/extensions/{transcript → layouts/context}/renderer/tool/diff/diff-presentation.ts +1 -1
  42. package/extensions/{transcript → layouts/context}/renderer/tool/diff/diff-renderer.ts +1 -1
  43. package/extensions/{transcript → layouts/context}/renderer/tool/diff/diff-write-render.ts +1 -1
  44. package/extensions/{transcript → layouts/context}/renderer/tool/diff/index.ts +2 -2
  45. package/extensions/{transcript → layouts/context}/renderer/tool/grouping.ts +4 -4
  46. package/extensions/{transcript → layouts/context}/renderer/tool/message-display.ts +6 -4
  47. package/extensions/{transcript → layouts/context}/renderer/tool/names.ts +2 -2
  48. package/extensions/{transcript → layouts/context}/renderer/tool/result.ts +3 -3
  49. package/extensions/{features/agent-summary → layouts/context/summary}/core.ts +1 -1
  50. package/extensions/{features → layouts/context/thinking}/compact-thinking.ts +7 -7
  51. package/extensions/{shell → layouts/editor}/accent-rail-editor.ts +8 -8
  52. package/extensions/{shell → layouts/editor}/completion-menu.ts +3 -6
  53. package/extensions/layouts/editor/controller.ts +613 -0
  54. package/extensions/{shell → layouts/editor}/editor-metadata-format.ts +6 -3
  55. package/extensions/layouts/editor/factory.ts +159 -0
  56. package/extensions/layouts/editor/index.ts +16 -0
  57. package/extensions/{shell → layouts/editor}/minimalist-editor.ts +4 -4
  58. package/extensions/layouts/editor/ownership.ts +103 -0
  59. package/extensions/{shell → layouts/editor}/ui.ts +2 -2
  60. package/extensions/layouts/footer/controller.ts +392 -0
  61. package/extensions/layouts/footer/data.ts +41 -0
  62. package/extensions/{shell → layouts/footer}/extension-status.ts +2 -2
  63. package/extensions/{shell → layouts/footer}/footer-layout.ts +1 -1
  64. package/extensions/{shell → layouts/footer}/footer.ts +11 -8
  65. package/extensions/layouts/footer/index.ts +24 -0
  66. package/extensions/layouts/header/index.ts +5 -0
  67. package/extensions/{features/shell → layouts/header}/startup-header.ts +105 -19
  68. package/extensions/layouts/working-line/controller.ts +291 -0
  69. package/extensions/{shell → layouts/working-line}/interaction-summary.ts +3 -3
  70. package/extensions/{shell → layouts/working-line}/working-line-spinners.ts +1 -1
  71. package/extensions/{shell → layouts/working-line}/working-line.ts +4 -4
  72. package/extensions/{shell → services}/live-context.ts +29 -1
  73. package/extensions/services/project-refresh.ts +374 -0
  74. package/extensions/{shell → services}/project-state.ts +5 -5
  75. package/extensions/{shell/state.ts → services/session-state.ts} +75 -5
  76. package/extensions/{shell → shared}/format.ts +4 -4
  77. package/extensions/tools/component-tree.ts +6 -6
  78. package/package.json +4 -4
  79. package/extensions/shell/index.ts +0 -1659
  80. package/extensions/shell/project-refresh.ts +0 -130
  81. package/extensions/shell/settings-command.ts +0 -2144
  82. package/extensions/transcript/index.ts +0 -65
  83. package/extensions/transcript/renderer/transcript-refresh.ts +0 -79
  84. /package/extensions/{shell → app/ownership}/prototype-patch-registry.ts +0 -0
  85. /package/extensions/{shell → app/runtime}/session-lifecycle.ts +0 -0
  86. /package/extensions/features/{shell/aliases.ts → aliases.ts} +0 -0
  87. /package/extensions/features/{shell → legacy}/working-message.ts +0 -0
  88. /package/extensions/features/{reference → session-reference}/index.ts +0 -0
  89. /package/extensions/features/{reference → session-reference}/session.ts +0 -0
  90. /package/extensions/features/{reference/subagent.ts → subagent-autocomplete.ts} +0 -0
  91. /package/extensions/{shell → layouts/context/message}/user-message-osc.ts +0 -0
  92. /package/extensions/{transcript → layouts/context}/renderer/markdown-enhance.ts +0 -0
  93. /package/extensions/{transcript → layouts/context}/renderer/tool/diff/ATTRIBUTION.md +0 -0
  94. /package/extensions/{transcript → layouts/context}/renderer/tool/diff/ansi-utils.ts +0 -0
  95. /package/extensions/{transcript → layouts/context}/renderer/tool/diff/diff-header.ts +0 -0
  96. /package/extensions/{transcript → layouts/context}/renderer/tool/diff/diff-inline.ts +0 -0
  97. /package/extensions/{transcript → layouts/context}/renderer/tool/diff/diff-parse.ts +0 -0
  98. /package/extensions/{transcript → layouts/context}/renderer/tool/diff/diff-text.ts +0 -0
  99. /package/extensions/{transcript → layouts/context}/renderer/tool/diff/line-width-safety.ts +0 -0
  100. /package/extensions/{transcript → layouts/context}/renderer/tool/diff/shiki-highlight.ts +0 -0
  101. /package/extensions/{transcript → layouts/context}/renderer/tool/diff/write-execution.ts +0 -0
  102. /package/extensions/{transcript → layouts/context}/renderer/tool/show-more-hint.ts +0 -0
  103. /package/extensions/{features/agent-summary → layouts/context/summary}/index.ts +0 -0
  104. /package/extensions/{shell → layouts/editor}/accent-rail-layout-patch.ts +0 -0
  105. /package/extensions/{shell → layouts/editor}/editor-transfer.ts +0 -0
  106. /package/extensions/{shell → layouts/footer}/footer-format.ts +0 -0
  107. /package/extensions/{shell → layouts/working-line}/working-line-messages.ts +0 -0
  108. /package/extensions/{shell/git.ts → services/git-data.ts} +0 -0
  109. /package/extensions/{shell/package-version.ts → services/package-data.ts} +0 -0
  110. /package/extensions/{shell/runtime.ts → services/runtime-data.ts} +0 -0
  111. /package/extensions/{shell → services}/telemetry.ts +0 -0
  112. /package/extensions/{shell → shared}/icons.ts +0 -0
  113. /package/extensions/{shell → shared}/style.ts +0 -0
@@ -13,47 +13,59 @@ const ANSI_RESET = "\x1b[0m";
13
13
  const ANSI_PATTERN =
14
14
  /[\u001B\u009B][[\]()#;?]*(?:(?:(?:[a-zA-Z\d]*(?:;[a-zA-Z\d]*)*)?\u0007)|(?:(?:\d{1,4}(?:;\d{0,4})*)?[\dA-PR-TZcf-nq-uy=><~]))/g;
15
15
 
16
- // 官方 install.sh 静态 logo(4 行原样,短行补尾随空格统一到 8 列)+ 底部空行补到 5 行,
17
- // 与右侧 tips 行数等高;着色保持现状(accent 渐变)
16
+ // Official install.sh static logo. Short rows are padded to eight cells,
17
+ // and one empty row keeps the logo as tall as the tips column.
18
18
  const LOGO_LINES = ["██████ ", "██ ██ ", "████ ██", "██ ██", " "];
19
19
 
20
- // hero 文案(单行,替换原生 header 的 "Pi can explain..." 默认位置)
20
+ // Single-line hero copy replacing Pi's default startup sentence.
21
21
  const HERO_PREFIX = "There are many agent harnesses, but this one is ";
22
22
  const HERO_HIGHLIGHT = "yours";
23
23
  const HERO_SUFFIX = ".";
24
24
 
25
- // 左右双栏布局:左侧 logo(5 行),右侧原生提示(5 行)
25
+ // Two-column layout: five logo rows on the left and five tips rows on the right.
26
26
  const TWO_COL_GAP = 2;
27
- // 窄于该宽度回退为垂直堆叠(logo + hero)
27
+ // Narrower viewports fall back to a vertical logo and hero layout.
28
28
  const TWO_COL_MIN_WIDTH = 48;
29
29
 
30
30
  const FALLBACK_ACCENT_RGB: Rgb = [80, 160, 255];
31
31
  const LOGO_BLOCK_WIDTH = Math.max(
32
32
  ...LOGO_LINES.map((line) => [...line].length),
33
33
  );
34
- // 左栏宽度 = logo 宽,右侧栏从该宽度后开始
34
+ // The tips column starts after the fixed logo width and gap.
35
35
  const LEFT_COLUMN_WIDTH = LOGO_BLOCK_WIDTH;
36
36
 
37
37
  const PALETTE_STEPS = 24;
38
38
  const PALETTE_MAX_DARKEN = 0.18;
39
39
  const PALETTE_MAX_LIGHTEN = 0.18;
40
- // 行内渐变波长:1/4 全波长(暗→亮单调,避免小字符数行内来回跳变)
40
+ // The row gradient uses a monotonic quarter-wave to avoid oscillation on short rows.
41
41
  const PALETTE_SPAN = 0.25;
42
- // 行间相位偏移:小步累加 → 整体左上暗→右下亮的对角渐变
42
+ // A small phase offset between rows creates a diagonal top-left to bottom-right gradient.
43
43
  const LOGO_ROW_PHASE_STEP = 0.08;
44
44
 
45
+ /**
46
+ * Removes terminal styling sequences before measuring visible text.
47
+ */
45
48
  function stripAnsi(text: string): string {
46
49
  return text.replace(ANSI_PATTERN, "");
47
50
  }
48
51
 
52
+ /**
53
+ * Returns the number of visible cells in a terminal string.
54
+ */
49
55
  function getVisibleLength(text: string): number {
50
56
  return [...stripAnsi(text)].length;
51
57
  }
52
58
 
59
+ /**
60
+ * Clamps a color channel to the byte range accepted by truecolor ANSI.
61
+ */
53
62
  function clampByte(value: number): number {
54
63
  return Math.max(0, Math.min(255, Math.round(value)));
55
64
  }
56
65
 
66
+ /**
67
+ * Interpolates one color channel between two byte values.
68
+ */
57
69
  function interpolateChannel(
58
70
  start: number,
59
71
  end: number,
@@ -62,6 +74,9 @@ function interpolateChannel(
62
74
  return Math.round(start + (end - start) * factor);
63
75
  }
64
76
 
77
+ /**
78
+ * Interpolates all channels between two RGB colors.
79
+ */
65
80
  function interpolateRgb(start: Rgb, end: Rgb, factor: number): Rgb {
66
81
  return [
67
82
  interpolateChannel(start[0], end[0], factor),
@@ -70,6 +85,9 @@ function interpolateRgb(start: Rgb, end: Rgb, factor: number): Rgb {
70
85
  ];
71
86
  }
72
87
 
88
+ /**
89
+ * Darkens an RGB color by a normalized amount.
90
+ */
73
91
  function darkenRgb(rgb: Rgb, amount: number): Rgb {
74
92
  return [
75
93
  clampByte(rgb[0] * (1 - amount)),
@@ -78,6 +96,9 @@ function darkenRgb(rgb: Rgb, amount: number): Rgb {
78
96
  ];
79
97
  }
80
98
 
99
+ /**
100
+ * Lightens an RGB color by a normalized amount.
101
+ */
81
102
  function lightenRgb(rgb: Rgb, amount: number): Rgb {
82
103
  return [
83
104
  clampByte(rgb[0] + (255 - rgb[0]) * amount),
@@ -86,11 +107,17 @@ function lightenRgb(rgb: Rgb, amount: number): Rgb {
86
107
  ];
87
108
  }
88
109
 
110
+ /**
111
+ * Wraps text in a truecolor foreground sequence and a reset.
112
+ */
89
113
  function applyTruecolor(rgb: Rgb, text: string): string {
90
114
  const [red, green, blue] = rgb;
91
115
  return `\x1b[38;2;${red};${green};${blue}m${text}${ANSI_RESET}`;
92
116
  }
93
117
 
118
+ /**
119
+ * Parses a truecolor foreground from an ANSI sequence.
120
+ */
94
121
  function parseTruecolorAnsi(ansi: string): Rgb | undefined {
95
122
  const match = ansi.match(/38;2;(\d+);(\d+);(\d+)/);
96
123
  if (!match) return undefined;
@@ -98,6 +125,9 @@ function parseTruecolorAnsi(ansi: string): Rgb | undefined {
98
125
  return [Number(match[1]), Number(match[2]), Number(match[3])];
99
126
  }
100
127
 
128
+ /**
129
+ * Parses an ANSI 256-color foreground and converts it to RGB.
130
+ */
101
131
  function parseAnsi256Foreground(ansi: string): Rgb | undefined {
102
132
  const match = ansi.match(/38;5;(\d+)/);
103
133
  if (!match) return undefined;
@@ -105,6 +135,9 @@ function parseAnsi256Foreground(ansi: string): Rgb | undefined {
105
135
  return ansi256ToRgb(Number(match[1]));
106
136
  }
107
137
 
138
+ /**
139
+ * Parses a standard or bright ANSI foreground and converts it to RGB.
140
+ */
108
141
  function parseAnsi16Foreground(ansi: string): Rgb | undefined {
109
142
  const normalMatch = ansi.match(/(?:\[|;)(3[0-7])(?:;|m)/);
110
143
  if (normalMatch) {
@@ -119,6 +152,9 @@ function parseAnsi16Foreground(ansi: string): Rgb | undefined {
119
152
  return undefined;
120
153
  }
121
154
 
155
+ /**
156
+ * Resolves the first supported foreground color found in an ANSI sequence.
157
+ */
122
158
  function parseForegroundRgbFromAnsi(ansi: string): Rgb | undefined {
123
159
  return (
124
160
  parseTruecolorAnsi(ansi) ??
@@ -127,12 +163,18 @@ function parseForegroundRgbFromAnsi(ansi: string): Rgb | undefined {
127
163
  );
128
164
  }
129
165
 
166
+ /**
167
+ * Resolves the theme accent color, falling back to a stable blue.
168
+ */
130
169
  function resolveAccentRgb(theme: { getFgAnsi(name: string): string }): Rgb {
131
170
  return (
132
171
  parseForegroundRgbFromAnsi(theme.getFgAnsi("accent")) ?? FALLBACK_ACCENT_RGB
133
172
  );
134
173
  }
135
174
 
175
+ /**
176
+ * Builds the cyclic dark-to-light palette used by the logo gradient.
177
+ */
136
178
  function buildAccentPalette(accent: Rgb): Rgb[] {
137
179
  return Array.from({ length: PALETTE_STEPS }, (_, index) => {
138
180
  const progress = index / PALETTE_STEPS;
@@ -146,6 +188,9 @@ function buildAccentPalette(accent: Rgb): Rgb[] {
146
188
  });
147
189
  }
148
190
 
191
+ /**
192
+ * Samples one wrapped position from an RGB palette.
193
+ */
149
194
  function sampleGradientColor(palette: Rgb[], position: number): Rgb {
150
195
  const wrappedPosition = ((position % 1) + 1) % 1;
151
196
  const scaledPosition = wrappedPosition * palette.length;
@@ -156,6 +201,9 @@ function sampleGradientColor(palette: Rgb[], position: number): Rgb {
156
201
  return interpolateRgb(palette[baseIndex]!, palette[nextIndex]!, factor);
157
202
  }
158
203
 
204
+ /**
205
+ * Applies a diagonal accent gradient to non-space characters.
206
+ */
159
207
  function renderGradientText(
160
208
  text: string,
161
209
  palette: Rgb[],
@@ -167,7 +215,7 @@ function renderGradientText(
167
215
  return characters
168
216
  .map((character, index) => {
169
217
  if (character === " ") return character;
170
- // 行内单调暗→亮(1/4 波长),行间相位偏移 → 对角渐变
218
+ // Use a monotonic quarter-wave within each row and phase-shift rows.
171
219
  const color = sampleGradientColor(
172
220
  palette,
173
221
  (index / span) * PALETTE_SPAN + phase,
@@ -177,6 +225,9 @@ function renderGradientText(
177
225
  .join("");
178
226
  }
179
227
 
228
+ /**
229
+ * Centers a fixed-width block within the available viewport.
230
+ */
180
231
  function createCenteredBlockLine(
181
232
  text: string,
182
233
  width: number,
@@ -186,6 +237,9 @@ function createCenteredBlockLine(
186
237
  return `${" ".repeat(leftPadding)}${text}`;
187
238
  }
188
239
 
240
+ /**
241
+ * Centers styled parts while measuring their unstyled representation.
242
+ */
189
243
  function createCenteredStyledLine(parts: StyledPart[], width: number): string {
190
244
  const rawText = parts.map((part) => part.raw).join("");
191
245
  const leftPadding = Math.max(
@@ -196,6 +250,9 @@ function createCenteredStyledLine(parts: StyledPart[], width: number): string {
196
250
  return `${" ".repeat(leftPadding)}${styledText}`;
197
251
  }
198
252
 
253
+ /**
254
+ * Truncates a rendered line without allowing ANSI escapes to affect width.
255
+ */
199
256
  function fitLineToWidth(line: string, width: number): string {
200
257
  if (getVisibleLength(line) <= width) {
201
258
  return line;
@@ -204,6 +261,9 @@ function fitLineToWidth(line: string, width: number): string {
204
261
  return stripAnsi(line).slice(0, width);
205
262
  }
206
263
 
264
+ /**
265
+ * Renders all logo rows at the requested width and gradient phase.
266
+ */
207
267
  function renderLogoLines(width: number, palette: Rgb[]): string[] {
208
268
  return LOGO_LINES.map((line, rowIndex) => {
209
269
  const phasedLine = renderGradientText(
@@ -215,15 +275,21 @@ function renderLogoLines(width: number, palette: Rgb[]): string[] {
215
275
  });
216
276
  }
217
277
 
218
- // ---- 右侧:原生默认 header 文本(对齐 pi 内置 startup header,按键文本随用户 keybindings 动态渲染) ----
278
+ // The right column mirrors Pi's startup hints and reads active keybindings.
219
279
 
280
+ /**
281
+ * Normalizes one keybinding part for the current operating system.
282
+ */
220
283
  function formatKeyPart(part: string): string {
221
- // 与 pi 内置 keybinding-hints 一致:macOS 上 alt 显示为 option
284
+ // Match Pi's keybinding hints: macOS displays alt as option.
222
285
  return process.platform === "darwin" && part.toLowerCase() === "alt"
223
286
  ? "option"
224
287
  : part;
225
288
  }
226
289
 
290
+ /**
291
+ * Formats a slash- or plus-separated keybinding for display.
292
+ */
227
293
  function formatKeyText(key: string): string {
228
294
  return key
229
295
  .split("/")
@@ -231,16 +297,22 @@ function formatKeyText(key: string): string {
231
297
  .join("/");
232
298
  }
233
299
 
300
+ /**
301
+ * Resolves the configured display text for one application keybinding.
302
+ */
234
303
  function keyText(keybinding: AppKeybinding): string {
235
304
  const keys = getKeybindings().getKeys(keybinding);
236
305
  return keys.length === 0 ? "" : formatKeyText(keys.join("/"));
237
306
  }
238
307
 
308
+ /**
309
+ * Renders the native startup hints shown beside the logo.
310
+ */
239
311
  function renderNativeLines(theme: {
240
312
  fg(name: string, text: string): string;
241
313
  bold(text: string): string;
242
314
  }): string[] {
243
- // 颜色方案照抄 pi 内置 header:按键 dim、描述 muted、分隔符 muted、版本 dim
315
+ // Match Pi's header palette: dim keys, muted descriptions and separators.
244
316
  const hint = (keybinding: AppKeybinding, description: string) =>
245
317
  theme.fg("dim", keyText(keybinding)) + theme.fg("muted", ` ${description}`);
246
318
  const rawHint = (key: string, description: string) =>
@@ -264,13 +336,16 @@ function renderNativeLines(theme: {
264
336
  `Press ${keyText("app.tools.expand")} to show full startup help and loaded resources.`,
265
337
  ),
266
338
  "",
267
- // hero 文案替换原生 "Pi can explain..." 行位置
339
+ // Replace the native "Pi can explain..." row with the hero sentence.
268
340
  renderHeroParts(theme)
269
341
  .map((part) => part.styled)
270
342
  .join(""),
271
343
  ];
272
344
  }
273
345
 
346
+ /**
347
+ * Builds the styled hero sentence and its raw-width counterparts.
348
+ */
274
349
  function renderHeroParts(theme: {
275
350
  fg(name: string, text: string): string;
276
351
  bold(text: string): string;
@@ -285,6 +360,13 @@ function renderHeroParts(theme: {
285
360
  ];
286
361
  }
287
362
 
363
+ /**
364
+ * Renders the responsive startup header for one viewport width.
365
+ *
366
+ * @param width Available terminal width in cells.
367
+ * @param theme Theme functions used for styling and accent discovery.
368
+ * @returns Header rows that fit the requested layout.
369
+ */
288
370
  export function renderHeaderLines(
289
371
  width: number,
290
372
  theme: {
@@ -297,7 +379,7 @@ export function renderHeaderLines(
297
379
  const palette = buildAccentPalette(accentRgb);
298
380
 
299
381
  if (width < TWO_COL_MIN_WIDTH) {
300
- // 窄屏回退:logo + hero 单行垂直堆叠居中
382
+ // Narrow fallback: vertically stack the centered logo and hero.
301
383
  const logoLines = renderLogoLines(width, palette);
302
384
  const heroLine = createCenteredStyledLine(renderHeroParts(theme), width);
303
385
  return ["", ...logoLines, "", heroLine, ""].map((line) =>
@@ -305,7 +387,7 @@ export function renderHeaderLines(
305
387
  );
306
388
  }
307
389
 
308
- // 双栏:左官方 logo(5 行) 右原生提示(5 行),同高并排,gap 分隔不交叉
390
+ // Wide layout: place the equal-height logo and native tips columns side by side.
309
391
  const leftLines = renderLogoLines(LEFT_COLUMN_WIDTH, palette);
310
392
  const rightLines = renderNativeLines(theme);
311
393
  const rightWidth = width - LEFT_COLUMN_WIDTH - TWO_COL_GAP;
@@ -326,13 +408,14 @@ export function renderHeaderLines(
326
408
  }
327
409
 
328
410
  /**
329
- * 按配置应用启动头:on → 自定义 header;off → 恢复官方默认 header。
330
- * 导出供 /ccstyle 面板在切换开关时实时重应用。
411
+ * Applies the configured startup header or restores Pi's native header.
412
+ *
413
+ * @param ctx Active extension context supplied by Pi.
331
414
  */
332
415
  export function applyStartupHeader(ctx: any): void {
333
416
  if (!ctx?.hasUI || typeof ctx.ui?.setHeader !== "function") return;
334
417
  if (!config.showStartupHeader) {
335
- // 恢复官方内置 header(logo + 快捷键提示 + onboarding)。
418
+ // Restore Pi's built-in logo, key hints and onboarding content.
336
419
  ctx.ui.setHeader(undefined);
337
420
  return;
338
421
  }
@@ -344,7 +427,10 @@ export function applyStartupHeader(ctx: any): void {
344
427
  }));
345
428
  }
346
429
 
347
- export default function piStartupHeader(pi: ExtensionAPI) {
430
+ /**
431
+ * Registers the Header layout lifecycle with the Pi extension host.
432
+ */
433
+ export default function registerHeaderLayout(pi: ExtensionAPI) {
348
434
  pi.on("session_start", async (_event, ctx) => {
349
435
  applyStartupHeader(ctx);
350
436
  });
@@ -0,0 +1,291 @@
1
+ import type {
2
+ ExtensionAPI,
3
+ ExtensionContext,
4
+ Theme,
5
+ } from "@earendil-works/pi-coding-agent";
6
+ import {
7
+ InteractionMetricsTracker,
8
+ TURN_SUMMARY_ENTRY_TYPE,
9
+ type MetricsUpdateResult,
10
+ } from "./interaction-summary.ts";
11
+ import {
12
+ AgentDurationClock,
13
+ snapshotWorkingLineHighStyle,
14
+ WorkingLineController,
15
+ } from "./working-line.ts";
16
+ import {
17
+ type PolishedTuiConfig,
18
+ type WorkingLineComponentPatch,
19
+ } from "../../app/config/shell.ts";
20
+ import type { SessionLifecycle } from "../../app/runtime/session-lifecycle.ts";
21
+
22
+ export type WorkingLineMessage = Parameters<
23
+ InteractionMetricsTracker["messageUpdate"]
24
+ >[0];
25
+ export type WorkingLineMessageEnd = Parameters<
26
+ InteractionMetricsTracker["messageEnd"]
27
+ >[0];
28
+
29
+ export type WorkingLineLayoutContext = {
30
+ readonly pi: ExtensionAPI;
31
+ readonly getConfig: () => PolishedTuiConfig;
32
+ readonly saveComponent: (
33
+ patch: WorkingLineComponentPatch,
34
+ ) => PolishedTuiConfig;
35
+ readonly getTheme: () => Theme;
36
+ readonly sessionLifecycle: SessionLifecycle;
37
+ readonly refresh: () => void;
38
+ readonly onAgentActiveChanged: (active: boolean) => void;
39
+ };
40
+
41
+ /**
42
+ * Owns WorkingLine lifecycle, interaction metrics, timing, and summary writes.
43
+ */
44
+ export class WorkingLineLayoutController {
45
+ private readonly context: WorkingLineLayoutContext;
46
+ private readonly durationClock = new AgentDurationClock();
47
+ private readonly metrics = new InteractionMetricsTracker();
48
+ private readonly workingLine: WorkingLineController;
49
+ private agentRunActive = false;
50
+ private writeSummary = true;
51
+
52
+ /**
53
+ * Creates a WorkingLine controller with one timing and metrics owner.
54
+ *
55
+ * @param context Shared runtime services consumed by WorkingLine.
56
+ */
57
+ constructor(context: WorkingLineLayoutContext) {
58
+ this.context = context;
59
+ this.workingLine = new WorkingLineController(
60
+ context.getConfig,
61
+ context.getTheme,
62
+ this.durationClock,
63
+ Math.random,
64
+ Date.now,
65
+ () => this.metrics.currentThought(),
66
+ );
67
+ }
68
+
69
+ /**
70
+ * Starts a new WorkingLine session and clears interaction state.
71
+ *
72
+ * @param ctx Active Pi extension context.
73
+ */
74
+ startSession(ctx: ExtensionContext): void {
75
+ this.metrics.shutdown();
76
+ this.agentRunActive = false;
77
+ this.durationClock.reset();
78
+ this.context.onAgentActiveChanged(false);
79
+ this.workingLine.startSession(ctx);
80
+ }
81
+
82
+ /**
83
+ * Starts an agent run and installs the current working indicator.
84
+ *
85
+ * @param ctx Active Pi extension context.
86
+ */
87
+ agentStart(ctx: ExtensionContext): void {
88
+ const { interactionStarted } = this.metrics.agentStart();
89
+ this.agentRunActive = true;
90
+ if (interactionStarted) this.durationClock.start();
91
+ this.context.onAgentActiveChanged(true);
92
+ this.workingLine.startAgent(ctx);
93
+ this.context.refresh();
94
+ }
95
+
96
+ /**
97
+ * Starts a new turn response and selects its stable working message.
98
+ *
99
+ * @param ctx Active Pi extension context.
100
+ */
101
+ turnStart(ctx: ExtensionContext): void {
102
+ this.metrics.turnStart();
103
+ this.workingLine.startTurn(ctx);
104
+ }
105
+
106
+ /**
107
+ * Records an assistant message update and refreshes visible metrics.
108
+ *
109
+ * @param message Latest assistant message snapshot.
110
+ * @param event Optional provider streaming event.
111
+ * @param ctx Active Pi extension context.
112
+ * @returns Metrics changes produced by the update.
113
+ */
114
+ messageUpdate(
115
+ message: WorkingLineMessage,
116
+ event: Parameters<InteractionMetricsTracker["messageUpdate"]>[1],
117
+ ctx: ExtensionContext,
118
+ ): MetricsUpdateResult {
119
+ const result = this.metrics.messageUpdate(message, event);
120
+ if (result.usageChanged || result.thoughtChanged) {
121
+ this.workingLine.updateMetrics(
122
+ result.displayTokens,
123
+ this.metrics.currentThought(),
124
+ ctx,
125
+ );
126
+ }
127
+ return result;
128
+ }
129
+
130
+ /**
131
+ * Finalizes one assistant message and updates WorkingLine metrics.
132
+ *
133
+ * @param message Final assistant message.
134
+ * @param ctx Active Pi extension context.
135
+ * @returns Message acceptance and display metrics.
136
+ */
137
+ messageEnd(message: WorkingLineMessageEnd, ctx: ExtensionContext) {
138
+ const result = this.metrics.messageEnd(message);
139
+ if (result.status === "accepted") {
140
+ this.workingLine.updateMetrics(
141
+ result.displayTokens,
142
+ this.metrics.currentThought(),
143
+ ctx,
144
+ );
145
+ }
146
+ return result;
147
+ }
148
+
149
+ /**
150
+ * Ends an agent run while retaining settled interaction metrics.
151
+ *
152
+ * @param ctx Active Pi extension context.
153
+ */
154
+ agentEnd(ctx: ExtensionContext): void {
155
+ const displayTokens = this.metrics.currentDisplayTokens();
156
+ this.metrics.agentEnd();
157
+ this.agentRunActive = false;
158
+ this.context.onAgentActiveChanged(false);
159
+ this.workingLine.finishAgent(ctx);
160
+ this.workingLine.updateMetrics(
161
+ displayTokens,
162
+ this.metrics.currentThought(),
163
+ ctx,
164
+ );
165
+ }
166
+
167
+ /**
168
+ * Starts and finishes one tool activity in the WorkingLine.
169
+ *
170
+ * @param toolCallId Pi tool call identifier.
171
+ * @param toolName Display name of the tool.
172
+ * @param ctx Active Pi extension context.
173
+ */
174
+ toolStart(toolCallId: string, toolName: string, ctx: ExtensionContext): void {
175
+ this.workingLine.startTool(toolCallId, toolName, ctx);
176
+ }
177
+
178
+ /**
179
+ * Finishes one tool activity in the WorkingLine.
180
+ *
181
+ * @param toolCallId Pi tool call identifier.
182
+ * @param ctx Active Pi extension context.
183
+ */
184
+ toolEnd(toolCallId: string, ctx: ExtensionContext): void {
185
+ this.workingLine.finishTool(toolCallId, ctx);
186
+ }
187
+
188
+ /**
189
+ * Configures whether this controller writes the canonical turn summary.
190
+ *
191
+ * @param enabled Whether summary entries should be persisted.
192
+ */
193
+ setSummaryWriterEnabled(enabled: boolean): void {
194
+ this.writeSummary = enabled;
195
+ }
196
+
197
+ /**
198
+ * Settles completed runs and writes one canonical summary entry when enabled.
199
+ *
200
+ * @param ctx Active Pi extension context.
201
+ */
202
+ agentSettled(ctx: ExtensionContext): void {
203
+ const settled = this.metrics.settle(ctx.isIdle());
204
+ if (!settled) return;
205
+ this.agentRunActive = settled.nextStartedAt !== undefined;
206
+ if (settled.nextStartedAt === undefined) this.durationClock.finish();
207
+ else this.durationClock.start(settled.nextStartedAt);
208
+ this.context.onAgentActiveChanged(this.agentRunActive);
209
+ this.workingLine.settle(settled.nextTokens, settled.nextThought, ctx);
210
+ const config = this.context.getConfig().components.workingLine;
211
+ if (!this.writeSummary || !config.enabled || !config.turnSummary) return;
212
+ try {
213
+ this.context.pi.appendEntry(TURN_SUMMARY_ENTRY_TYPE, {
214
+ version: 3,
215
+ ...settled.summary,
216
+ stylePrefix: snapshotWorkingLineHighStyle(
217
+ ctx.ui.theme,
218
+ config,
219
+ this.context.getConfig().colors,
220
+ ),
221
+ });
222
+ } catch {
223
+ // Summary persistence failure must not break settlement cleanup.
224
+ }
225
+ }
226
+
227
+ /**
228
+ * Applies a WorkingLine configuration patch and reconciles the host row.
229
+ *
230
+ * @param patch WorkingLine configuration changes to persist.
231
+ * @param ctx Active Pi extension context.
232
+ * @returns Whether the host working row accepted the change.
233
+ */
234
+ setComponent(
235
+ patch: WorkingLineComponentPatch,
236
+ ctx: ExtensionContext,
237
+ ): { applied: boolean; reason?: string } {
238
+ this.context.saveComponent(patch);
239
+ return this.workingLine.reconcile(ctx);
240
+ }
241
+
242
+ /**
243
+ * Reconciles WorkingLine configuration without changing persisted settings.
244
+ *
245
+ * @param ctx Active Pi extension context.
246
+ * @returns Whether the host working row accepted the change.
247
+ */
248
+ reconcile(ctx: ExtensionContext) {
249
+ return this.workingLine.reconcile(ctx);
250
+ }
251
+
252
+ /**
253
+ * Disposes the WorkingLine and clears all interaction-local state.
254
+ *
255
+ * @param ctx Active Pi extension context.
256
+ */
257
+ dispose(ctx: ExtensionContext): void {
258
+ this.workingLine.dispose(ctx);
259
+ this.metrics.shutdown();
260
+ this.durationClock.reset();
261
+ this.agentRunActive = false;
262
+ this.context.onAgentActiveChanged(false);
263
+ }
264
+
265
+ /**
266
+ * Returns the shared duration clock used by Editor selectors.
267
+ *
268
+ * @returns Session-local duration clock.
269
+ */
270
+ duration(): AgentDurationClock {
271
+ return this.durationClock;
272
+ }
273
+
274
+ /**
275
+ * Reports whether the agent run is currently active.
276
+ *
277
+ * @returns Whether WorkingLine owns an active agent run.
278
+ */
279
+ isAgentActive(): boolean {
280
+ return this.agentRunActive;
281
+ }
282
+
283
+ /**
284
+ * Returns the currently active interaction thought snapshot.
285
+ *
286
+ * @returns Current thought metrics.
287
+ */
288
+ currentThought() {
289
+ return this.metrics.currentThought();
290
+ }
291
+ }
@@ -4,9 +4,9 @@ import type {
4
4
  } from "@earendil-works/pi-ai";
5
5
  import type { Theme } from "@earendil-works/pi-coding-agent";
6
6
  import { Text } from "@earendil-works/pi-tui";
7
- import type { ColorSource, ColorSpec } from "../app/config/shell.ts";
8
- import { formatCount, formatElapsedDuration } from "./format.ts";
9
- import { isSafeSgrStylePrefix } from "./style.ts";
7
+ import type { ColorSource, ColorSpec } from "../../app/config/shell.ts";
8
+ import { formatCount, formatElapsedDuration } from "../../shared/format.ts";
9
+ import { isSafeSgrStylePrefix } from "../../shared/style.ts";
10
10
  import { renderWorkingLineHigh } from "./working-line.ts";
11
11
 
12
12
  export const TURN_SUMMARY_ENTRY_TYPE = "zentui-turn-summary";
@@ -1,4 +1,4 @@
1
- import type { WorkingLineSpinner } from "../app/config/shell.ts";
1
+ import type { WorkingLineSpinner } from "../../app/config/shell.ts";
2
2
 
3
3
  /*
4
4
  * Pulse frames adapted from Funky UI's animations.ts:
@@ -6,15 +6,15 @@ import type {
6
6
  WorkingLineComponentConfig,
7
7
  WorkingLineTextAnimation,
8
8
  ZentuiConfig,
9
- } from "../app/config/shell.ts";
10
- import { formatCount } from "./format.ts";
9
+ } from "../../app/config/shell.ts";
10
+ import { formatCount } from "../../shared/format.ts";
11
11
  import {
12
12
  isSafeSgrStylePrefix,
13
13
  isSupportedColorSpec,
14
14
  renderStyleForSourceOrFallback,
15
15
  type SourceStyleFallback,
16
16
  type ThemeLike,
17
- } from "./style.ts";
17
+ } from "../../shared/style.ts";
18
18
  import { PI_WORKING_LINE_MESSAGES } from "./working-line-messages.ts";
19
19
  import { WORKING_LINE_SPINNERS } from "./working-line-spinners.ts";
20
20
 
@@ -1538,7 +1538,7 @@ export class WorkingLineController {
1538
1538
  if (snapshot.ownsMessage) ui.setWorkingMessage("");
1539
1539
  } catch {
1540
1540
  // Recovery is deliberately direct rather than recursive. If either setter cannot
1541
- // restore the last successful public state, release both unkeyed surfaces.
1541
+ // restore the last successful public state, release both unkeyed layouts.
1542
1542
  this.releaseAfterFailure(ui);
1543
1543
  }
1544
1544
  }