pi-cc-extensions 0.8.9 → 0.8.11

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 (42) hide show
  1. package/README.md +3 -2
  2. package/extensions/agent-autocomplete.ts +11 -16
  3. package/extensions/ask-user-question/ask-user-question.ts +29 -7
  4. package/extensions/ask-user-question/config.ts +6 -2
  5. package/extensions/ask-user-question/index.ts +8 -8
  6. package/extensions/ask-user-question/rpc-fallback.ts +16 -4
  7. package/extensions/ask-user-question/state/build-questionnaire.ts +16 -5
  8. package/extensions/ask-user-question/state/key-router.ts +29 -7
  9. package/extensions/ask-user-question/state/questionnaire-session.ts +1 -4
  10. package/extensions/ask-user-question/state/selectors/projections.ts +4 -1
  11. package/extensions/ask-user-question/state/state-reducer.ts +24 -7
  12. package/extensions/ask-user-question/tool/response-envelope.ts +4 -1
  13. package/extensions/ask-user-question/tool/types.ts +5 -1
  14. package/extensions/ask-user-question/tool/validate-questionnaire.ts +10 -2
  15. package/extensions/ask-user-question/view/components/inline-input.ts +15 -2
  16. package/extensions/ask-user-question/view/components/multi-select-view.ts +11 -4
  17. package/extensions/ask-user-question/view/components/option-list-view.ts +14 -5
  18. package/extensions/ask-user-question/view/components/preview/preview-block-renderer.ts +13 -7
  19. package/extensions/ask-user-question/view/components/preview/preview-layout-decider.ts +16 -4
  20. package/extensions/ask-user-question/view/components/preview/preview-pane.ts +5 -1
  21. package/extensions/ask-user-question/view/components/submit-picker.ts +3 -1
  22. package/extensions/ask-user-question/view/components/wrapping-select.ts +20 -4
  23. package/extensions/ask-user-question/view/dialog-builder.ts +27 -7
  24. package/extensions/ask-user-question/view/tab-content-strategy.ts +23 -8
  25. package/extensions/claude-code-style.ts +1722 -1485
  26. package/extensions/compact-style.ts +120 -43
  27. package/extensions/context.ts +502 -394
  28. package/extensions/session-reference-core.ts +8 -3
  29. package/extensions/session-reference.ts +475 -448
  30. package/extensions/tool-diff/ATTRIBUTION.md +28 -0
  31. package/extensions/tool-diff/ansi-utils.ts +113 -0
  32. package/extensions/tool-diff/diff-presentation.ts +70 -0
  33. package/extensions/tool-diff/diff-renderer.ts +2827 -0
  34. package/extensions/tool-diff/index.ts +106 -0
  35. package/extensions/tool-diff/line-width-safety.ts +91 -0
  36. package/extensions/tool-diff/render-utils.ts +5 -0
  37. package/extensions/tool-diff/types.ts +20 -0
  38. package/extensions/tool-diff/write-display-utils.ts +29 -0
  39. package/extensions/tool-diff/write-execution.ts +113 -0
  40. package/extensions/tool-result-sanitize.ts +4 -4
  41. package/package.json +64 -61
  42. package/themes/github-dark-default.json +91 -91
@@ -1,7 +1,10 @@
1
1
  import { visibleWidth } from "@earendil-works/pi-tui";
2
2
  import type { QuestionData } from "../../../tool/types.js";
3
3
  import type { WrappingSelectItem } from "../wrapping-select.js";
4
- import { BORDER_HORIZONTAL_OVERHEAD, BORDER_INNER_PADDING_HORIZONTAL } from "./preview-box-renderer.js";
4
+ import {
5
+ BORDER_HORIZONTAL_OVERHEAD,
6
+ BORDER_INNER_PADDING_HORIZONTAL,
7
+ } from "./preview-box-renderer.js";
5
8
 
6
9
  /** Min terminal/pane width for the side-by-side layout to engage. */
7
10
  export const PREVIEW_MIN_WIDTH = 100;
@@ -34,7 +37,9 @@ export type PreviewLayoutMode = "side-by-side" | "stacked";
34
37
  * capping height too short.
35
38
  */
36
39
  export function decideLayout(terminalWidth: number, paneWidth: number): PreviewLayoutMode {
37
- return terminalWidth >= PREVIEW_MIN_WIDTH && paneWidth >= PREVIEW_MIN_WIDTH ? "side-by-side" : "stacked";
40
+ return terminalWidth >= PREVIEW_MIN_WIDTH && paneWidth >= PREVIEW_MIN_WIDTH
41
+ ? "side-by-side"
42
+ : "stacked";
38
43
  }
39
44
 
40
45
  /**
@@ -120,12 +125,19 @@ export function previewSourceWidth(question: QuestionData): number {
120
125
  *
121
126
  * Pure function — same cross-tab max pattern as `crossTabMaxLeftWidth`.
122
127
  */
123
- export function crossTabPreviewBudget(questions: readonly QuestionData[], paneWidth: number): number {
128
+ export function crossTabPreviewBudget(
129
+ questions: readonly QuestionData[],
130
+ paneWidth: number,
131
+ ): number {
124
132
  let max = MIN_PREVIEW_WIDTH;
125
133
  for (const question of questions) {
126
134
  const rawWidth = previewSourceWidth(question);
127
135
  const capped = Math.min(rawWidth, paneWidth - PREVIEW_COLUMN_GAP - MIN_LEFT);
128
- const budget = capped + BORDER_HORIZONTAL_OVERHEAD + 2 * BORDER_INNER_PADDING_HORIZONTAL + PREVIEW_PADDING_LEFT;
136
+ const budget =
137
+ capped +
138
+ BORDER_HORIZONTAL_OVERHEAD +
139
+ 2 * BORDER_INNER_PADDING_HORIZONTAL +
140
+ PREVIEW_PADDING_LEFT;
129
141
  if (budget > max) max = budget;
130
142
  }
131
143
  return max;
@@ -168,7 +168,11 @@ export class PreviewPane implements StatefulView<PreviewPaneProps>, Component {
168
168
  const adaptiveLeft = this.getAdaptiveLeft(width);
169
169
  const { optionsWidth, previewWidth } = bodyWidths(width, mode, adaptiveLeft);
170
170
  const optionsHeight = this.optionListView.render(optionsWidth).length;
171
- const previewBlockHeight = this.previewBlock.blockHeight(previewWidth, this.props.selectedIndex, mode);
171
+ const previewBlockHeight = this.previewBlock.blockHeight(
172
+ previewWidth,
173
+ this.props.selectedIndex,
174
+ mode,
175
+ );
172
176
  if (mode === "side-by-side") return Math.max(optionsHeight, previewBlockHeight);
173
177
  return optionsHeight + STACKED_GAP_ROWS + previewBlockHeight;
174
178
  }
@@ -56,7 +56,9 @@ export class SubmitPicker implements StatefulView<SubmitPickerProps> {
56
56
  const active = this.props.rows[i]?.active ?? false;
57
57
  const pointer = active ? ACTIVE_POINTER : INACTIVE_POINTER;
58
58
  const number = `${i + 1}${NUMBER_SEPARATOR}`;
59
- const label = active ? this.theme.fg("accent", this.theme.bold(text)) : this.theme.fg("text", text);
59
+ const label = active
60
+ ? this.theme.fg("accent", this.theme.bold(text))
61
+ : this.theme.fg("text", text);
60
62
  lines.push(truncateToWidth(`${pointer}${number}${label}`, width, ""));
61
63
  }
62
64
  return lines;
@@ -192,7 +192,10 @@ export class WrappingSelect implements Component {
192
192
 
193
193
  private computeVisibleWindow(): { startIndex: number; endIndex: number } {
194
194
  const half = Math.floor(this.maxVisible / 2);
195
- const startIndex = Math.max(0, Math.min(this.selectedIndex - half, this.items.length - this.maxVisible));
195
+ const startIndex = Math.max(
196
+ 0,
197
+ Math.min(this.selectedIndex - half, this.items.length - this.maxVisible),
198
+ );
196
199
  const endIndex = Math.min(startIndex + this.maxVisible, this.items.length);
197
200
  return { startIndex, endIndex };
198
201
  }
@@ -210,7 +213,10 @@ export class WrappingSelect implements Component {
210
213
  ): string[] {
211
214
  const rowPrefix = this.buildRowPrefix(index, isActive, numberWidth);
212
215
  const continuationPrefix = " ".repeat(visibleWidth(rowPrefix));
213
- const contentWidth = Math.max(WrappingSelect.MIN_CONTENT_WIDTH, width - visibleWidth(rowPrefix));
216
+ const contentWidth = Math.max(
217
+ WrappingSelect.MIN_CONTENT_WIDTH,
218
+ width - visibleWidth(rowPrefix),
219
+ );
214
220
 
215
221
  if (this.shouldRenderAsInlineInput(item, isActive)) {
216
222
  return this.renderInlineInputRow(rowPrefix, continuationPrefix, contentWidth);
@@ -229,7 +235,13 @@ export class WrappingSelect implements Component {
229
235
  const applySelectedStyle = isActive || isConfirmed;
230
236
 
231
237
  return [
232
- ...this.renderLabelBlock(label, rowPrefix, continuationPrefix, contentWidth, applySelectedStyle),
238
+ ...this.renderLabelBlock(
239
+ label,
240
+ rowPrefix,
241
+ continuationPrefix,
242
+ contentWidth,
243
+ applySelectedStyle,
244
+ ),
233
245
  ...this.renderDescriptionBlock(item.description, continuationPrefix, contentWidth),
234
246
  ];
235
247
  }
@@ -252,7 +264,11 @@ export class WrappingSelect implements Component {
252
264
  * building core. The `multiline: true` path is byte-identical to the pre-extraction
253
265
  * output (cursor rationale lives in `inline-input.ts`).
254
266
  */
255
- private renderInlineInputRow(rowPrefix: string, continuationPrefix: string, contentWidth: number): string[] {
267
+ private renderInlineInputRow(
268
+ rowPrefix: string,
269
+ continuationPrefix: string,
270
+ contentWidth: number,
271
+ ): string[] {
256
272
  return renderInlineInputRow({
257
273
  buffer: this.inputBuffer,
258
274
  cursorOffset: this.inputCursorOffset,
@@ -6,7 +6,11 @@ import type { PreviewPaneProps } from "./components/preview/preview-pane.js";
6
6
  import type { TabBar } from "./components/tab-bar.js";
7
7
  import type { StatefulView } from "./stateful-view.js";
8
8
  import type { TabComponents } from "./tab-components.js";
9
- import { QuestionTabStrategy, SubmitTabStrategy, type TabContentStrategy } from "./tab-content-strategy.js";
9
+ import {
10
+ QuestionTabStrategy,
11
+ SubmitTabStrategy,
12
+ type TabContentStrategy,
13
+ } from "./tab-content-strategy.js";
10
14
 
11
15
  export const HINT_PART_ENTER = "Enter to select";
12
16
  export const HINT_PART_NAV = "↑/↓ to navigate";
@@ -27,10 +31,16 @@ export const HINT_PART_EXPAND = "Ctrl+] to expand";
27
31
  * prefix substring of the rendered line. On narrow terminals the collapse tail
28
32
  * clips with `…` (`OneLineClippedText`); the core is preserved.
29
33
  */
30
- export const HINT_SINGLE = [HINT_PART_ENTER, HINT_PART_NAV, HINT_PART_NOTES, HINT_PART_CANCEL].join(" · ");
31
- export const HINT_MULTI = [HINT_PART_ENTER, HINT_PART_NAV, HINT_PART_NOTES, HINT_PART_TAB, HINT_PART_CANCEL].join(
34
+ export const HINT_SINGLE = [HINT_PART_ENTER, HINT_PART_NAV, HINT_PART_NOTES, HINT_PART_CANCEL].join(
32
35
  " · ",
33
36
  );
37
+ export const HINT_MULTI = [
38
+ HINT_PART_ENTER,
39
+ HINT_PART_NAV,
40
+ HINT_PART_NOTES,
41
+ HINT_PART_TAB,
42
+ HINT_PART_CANCEL,
43
+ ].join(" · ");
34
44
  /** Single-line footer shown by `QuestionnaireSession` when `state.collapsed === true`. Bypasses `buildHintText`. */
35
45
  export const COLLAPSED_HINT = [HINT_PART_EXPAND, HINT_PART_CANCEL].join(" · ");
36
46
  export const REVIEW_HEADING = "Review your answers";
@@ -137,7 +147,10 @@ export class DialogView implements StatefulView<DialogProps> {
137
147
  const availableMiddle = Math.max(0, termRows - topFixed - bottomFixed);
138
148
  if (availableMiddle === 0) {
139
149
  // Terminal too small for any middle content — show just chrome.
140
- const chromeOnly = [...natural.slice(0, topFixed), ...natural.slice(natural.length - bottomFixed)];
150
+ const chromeOnly = [
151
+ ...natural.slice(0, topFixed),
152
+ ...natural.slice(natural.length - bottomFixed),
153
+ ];
141
154
  return chromeOnly.length > termRows ? chromeOnly.slice(0, termRows) : chromeOnly;
142
155
  }
143
156
 
@@ -149,14 +162,18 @@ export class DialogView implements StatefulView<DialogProps> {
149
162
  const focusedRowInMiddle = headingCount + bodyRange[0];
150
163
  const focusedHeight = bodyRange[1] - bodyRange[0];
151
164
  // Center the focused item vertically in the available middle space.
152
- const idealStart = focusedRowInMiddle - Math.floor(Math.max(0, availableMiddle - focusedHeight) / 2);
165
+ const idealStart =
166
+ focusedRowInMiddle - Math.floor(Math.max(0, availableMiddle - focusedHeight) / 2);
153
167
  scrollStart = Math.max(0, Math.min(idealStart, middleRows - availableMiddle));
154
168
  } else {
155
169
  // No interactive focus (submit tab) — top-anchor the middle.
156
170
  scrollStart = 0;
157
171
  }
158
172
 
159
- const scrollableMiddle = natural.slice(topFixed + scrollStart, topFixed + scrollStart + availableMiddle);
173
+ const scrollableMiddle = natural.slice(
174
+ topFixed + scrollStart,
175
+ topFixed + scrollStart + availableMiddle,
176
+ );
160
177
 
161
178
  const hasUp = scrollStart > 0;
162
179
  const hasDown = scrollStart + availableMiddle < middleRows;
@@ -182,7 +199,10 @@ export class DialogView implements StatefulView<DialogProps> {
182
199
  return result.length > termRows ? result.slice(0, termRows) : result;
183
200
  }
184
201
 
185
- private buildContainerFromStrategy(strategy: TabContentStrategy, headingRowCache: Component[]): Container {
202
+ private buildContainerFromStrategy(
203
+ strategy: TabContentStrategy,
204
+ headingRowCache: Component[],
205
+ ): Container {
186
206
  const { theme, isMulti, tabBar } = this.config;
187
207
  const state = this.liveProps.state;
188
208
  const container = new Container();
@@ -1,5 +1,12 @@
1
1
  import type { Theme } from "@earendil-works/pi-coding-agent";
2
- import { type Component, Container, type Input, Spacer, Text, truncateToWidth } from "@earendil-works/pi-tui";
2
+ import {
3
+ type Component,
4
+ Container,
5
+ type Input,
6
+ Spacer,
7
+ Text,
8
+ truncateToWidth,
9
+ } from "@earendil-works/pi-tui";
3
10
  import { formatAnswerScalar } from "../tool/format-answer.js";
4
11
  import type { QuestionData } from "../tool/types.js";
5
12
  import type { PreviewPane, PreviewPaneProps } from "./components/preview/preview-pane.js";
@@ -134,7 +141,10 @@ export class QuestionTabStrategy implements TabContentStrategy {
134
141
  // drops the trailing parts (collapse hint first, then cancel) with `…`.
135
142
  return [
136
143
  new Spacer(1),
137
- new OneLineClippedText(this.config.theme.fg("dim", buildHintText(question, this.config.isMulti, state)), 1),
144
+ new OneLineClippedText(
145
+ this.config.theme.fg("dim", buildHintText(question, this.config.isMulti, state)),
146
+ 1,
147
+ ),
138
148
  ];
139
149
  }
140
150
 
@@ -175,7 +185,11 @@ export class SubmitTabStrategy implements TabContentStrategy {
175
185
  const answerText = formatAnswerScalar(a, "summary");
176
186
  c.addChild(new Text(this.config.theme.fg("muted", ` ● ${label}`), 1, 0));
177
187
  c.addChild(
178
- new Text(` ${this.config.theme.fg("muted", "→")} ${this.config.theme.fg("text", answerText)}`, 1, 0),
188
+ new Text(
189
+ ` ${this.config.theme.fg("muted", "→")} ${this.config.theme.fg("text", answerText)}`,
190
+ 1,
191
+ 0,
192
+ ),
179
193
  );
180
194
  if (a.notes && a.notes.length > 0) {
181
195
  c.addChild(new Text(this.config.theme.fg("dim", ` notes: ${a.notes}`), 1, 0));
@@ -203,10 +217,7 @@ export class SubmitTabStrategy implements TabContentStrategy {
203
217
  const promptText =
204
218
  missing.length === 0
205
219
  ? this.config.theme.fg("muted", READY_PROMPT)
206
- : this.config.theme.fg(
207
- "warning",
208
- `${INCOMPLETE_WARNING_PREFIX} ${missing.join(", ")}`,
209
- );
220
+ : this.config.theme.fg("warning", `${INCOMPLETE_WARNING_PREFIX} ${missing.join(", ")}`);
210
221
  const out: Component[] = [new Spacer(1), new Text(promptText, 1, 0), new Spacer(1)];
211
222
  if (this.config.submitPicker) {
212
223
  out.push(this.config.submitPicker);
@@ -236,7 +247,11 @@ export class SubmitTabStrategy implements TabContentStrategy {
236
247
  * is the trade we picked over wrapping (which would inflate `footerRowCount` and
237
248
  * desync the height math in `DialogView.render`).
238
249
  */
239
- export function buildHintText(question: QuestionData | undefined, isMulti: boolean, state: DialogState): string {
250
+ export function buildHintText(
251
+ question: QuestionData | undefined,
252
+ isMulti: boolean,
253
+ state: DialogState,
254
+ ): string {
240
255
  const parts: string[] = [HINT_PART_ENTER, HINT_PART_NAV];
241
256
  if (question?.multiSelect === true) parts.push(HINT_PART_TOGGLE);
242
257
  if (question && !state.notesVisible && !state.inputMode) {