@ff-labs/pi-fff 0.10.7-nightly.e8d80e4 → 0.11.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 (2) hide show
  1. package/package.json +3 -3
  2. package/src/index.ts +124 -26
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@ff-labs/pi-fff",
3
3
  "public": true,
4
- "version": "0.10.7-nightly.e8d80e4",
4
+ "version": "0.11.0",
5
5
  "description": "pi extension: FFF-powered fuzzy file and content search",
6
6
  "type": "module",
7
7
  "license": "MIT",
@@ -41,8 +41,8 @@
41
41
  "typecheck": "tsc --noEmit"
42
42
  },
43
43
  "dependencies": {
44
- "@ff-labs/fff-bun": "0.10.7-nightly.e8d80e4",
45
- "@ff-labs/fff-node": "0.10.7-nightly.e8d80e4"
44
+ "@ff-labs/fff-bun": "0.11.0",
45
+ "@ff-labs/fff-node": "0.11.0"
46
46
  },
47
47
  "peerDependencies": {
48
48
  "@earendil-works/pi-coding-agent": "*",
package/src/index.ts CHANGED
@@ -14,7 +14,10 @@ import type {
14
14
  import {
15
15
  type AutocompleteItem,
16
16
  type AutocompleteProvider,
17
+ type Component,
18
+ sliceByColumn,
17
19
  Text,
20
+ visibleWidth,
18
21
  } from "@earendil-works/pi-tui";
19
22
  import type {
20
23
  FileFinderApi,
@@ -655,23 +658,51 @@ export default function fffExtension(pi: ExtensionAPI) {
655
658
  };
656
659
 
657
660
  const pendingTools: (() => string)[] = [];
661
+ const registeredToolNames = new Set<string>();
662
+ // A renderer is attached to a concrete registered name. Keep that name outside
663
+ // row state so old fffgrep rows retain their title after mode changes to override.
664
+ const renderToolNames = new WeakMap<object, string>();
658
665
  let toolsRegistered = false;
659
666
 
667
+ function getRenderToolName(context: object, fallback: string): string {
668
+ return renderToolNames.get(context) ?? fallback;
669
+ }
670
+
671
+ function registerTool<TParams extends TSchema, TDetails = unknown, TState = any>(
672
+ resolveName: () => string,
673
+ definition: PendingToolDefinition<TParams, TDetails, TState>,
674
+ ): string {
675
+ const resolvedName = resolveName();
676
+ if (registeredToolNames.has(resolvedName)) return resolvedName;
677
+
678
+ const { promptGuidelines, renderCall, ...tool } = definition;
679
+ pi.registerTool({
680
+ ...tool,
681
+ name: resolvedName,
682
+ label: resolvedName,
683
+ promptGuidelines: promptGuidelines?.(toolNames),
684
+ renderCall: renderCall
685
+ ? (args, theme, context) => {
686
+ renderToolNames.set(context, resolvedName);
687
+ return renderCall(args, theme, context);
688
+ }
689
+ : undefined,
690
+ });
691
+ registeredToolNames.add(resolvedName);
692
+ return resolvedName;
693
+ }
694
+
660
695
  function queueTool<TParams extends TSchema, TDetails = unknown, TState = any>(
661
696
  resolveName: () => string,
662
697
  definition: PendingToolDefinition<TParams, TDetails, TState>,
663
698
  ): void {
664
- pendingTools.push(() => {
665
- const { promptGuidelines, ...tool } = definition;
666
- const resolvedName = resolveName();
667
- pi.registerTool({
668
- ...tool,
669
- name: resolvedName,
670
- label: resolvedName,
671
- promptGuidelines: promptGuidelines?.(toolNames),
672
- });
673
- return resolvedName;
674
- });
699
+ pendingTools.push(() => registerTool(resolveName, definition));
700
+
701
+ // Pi restores historical tool rows before session_start. Register the
702
+ // FFF-named tools now so their renderers resolve. Pi activates every
703
+ // newly registered tool, so registerPendingTools prunes the names the
704
+ // final mode did not select.
705
+ registerTool(resolveName, definition);
675
706
  }
676
707
 
677
708
  // Pi carries the active tool list across /reload, so names activated under a
@@ -829,7 +860,72 @@ export default function fffExtension(pi: ExtensionAPI) {
829
860
 
830
861
  // --- Shared render helpers ---
831
862
 
832
- const renderTextResult = (
863
+ class CollapsedText implements Component {
864
+ constructor(
865
+ private readonly preview: string,
866
+ private readonly suffix: string,
867
+ private readonly marker: string,
868
+ ) {}
869
+
870
+ render(width: number): string[] {
871
+ const availableWidth = Math.max(1, width);
872
+ if (!this.suffix) {
873
+ if (visibleWidth(this.preview) <= availableWidth) return [this.preview];
874
+ const markerWidth = visibleWidth(this.marker);
875
+ if (markerWidth >= availableWidth) {
876
+ return [sliceByColumn(this.marker, 0, availableWidth, true)];
877
+ }
878
+ return [
879
+ `${sliceByColumn(this.preview, 0, availableWidth - markerWidth, true)}${this.marker}`,
880
+ ];
881
+ }
882
+
883
+ const suffixWidth = visibleWidth(this.suffix);
884
+ if (suffixWidth >= availableWidth) {
885
+ return [sliceByColumn(this.suffix, 0, availableWidth, true)];
886
+ }
887
+
888
+ const previewWidth = availableWidth - suffixWidth - 1;
889
+ if (visibleWidth(this.preview) <= previewWidth) {
890
+ return [`${this.preview} ${this.suffix}`];
891
+ }
892
+
893
+ const markerWidth = visibleWidth(this.marker);
894
+ return [
895
+ `${sliceByColumn(this.preview, 0, Math.max(0, previewWidth - markerWidth), true)}${this.marker} ${this.suffix}`,
896
+ ];
897
+ }
898
+
899
+ invalidate(): void {}
900
+ }
901
+
902
+ // Pi wraps returned components in its own click-to-expand MouseRegion and passes
903
+ // the toggled state via options.expanded, so no custom mouse handling is needed.
904
+ const renderCompactTextResult = (
905
+ result: { content?: { type: string; text?: string }[] },
906
+ options: { expanded?: boolean },
907
+ theme: any,
908
+ context: any,
909
+ ): Component => {
910
+ const output = result.content?.find((c) => c.type === "text")?.text?.trim() ?? "";
911
+ if (!output) return new Text(theme.fg("muted", "No output"), 0, 0);
912
+
913
+ const lines = output.split("\n");
914
+ const color = context.isError ? "error" : "toolOutput";
915
+ if (options.expanded) {
916
+ return new Text(lines.map((line) => theme.fg(color, line)).join("\n"), 0, 0);
917
+ }
918
+
919
+ const suffix =
920
+ lines.length > 1 ? theme.fg("muted", `... (${lines.length - 1} more lines)`) : "";
921
+ return new CollapsedText(
922
+ theme.fg(color, lines[0] ?? ""),
923
+ suffix,
924
+ theme.fg("muted", "..."),
925
+ );
926
+ };
927
+
928
+ const renderPreviewResult = (
833
929
  result: { content?: { type: string; text?: string }[] },
834
930
  options: { expanded?: boolean },
835
931
  theme: any,
@@ -1036,23 +1132,24 @@ export default function fffExtension(pi: ExtensionAPI) {
1036
1132
  },
1037
1133
 
1038
1134
  renderCall(args, theme, context) {
1039
- const text = (context.lastComponent as Text | undefined) ?? new Text("", 0, 0);
1040
1135
  const pattern = args?.pattern ?? "";
1041
1136
  const path = args?.path ?? ".";
1042
1137
  let content =
1043
- theme.fg("toolTitle", theme.bold(toolNames.grep)) +
1138
+ theme.fg("toolTitle", theme.bold(getRenderToolName(context, toolNames.grep))) +
1044
1139
  " " +
1045
1140
  theme.fg("accent", `/${pattern}/`) +
1046
1141
  theme.fg("toolOutput", ` in ${path}`);
1047
- if (args?.limit !== undefined)
1048
- content += theme.fg("toolOutput", ` limit ${args.limit}`);
1142
+ const options: string[] = [];
1143
+ if (args?.limit !== undefined) options.push(`limit ${args.limit}`);
1144
+ if (args?.context !== undefined) options.push(`context ${args.context}`);
1145
+ if (options.length > 0)
1146
+ content += theme.fg("toolOutput", ` (${options.join(", ")})`);
1049
1147
  if (args?.cursor) content += theme.fg("muted", ` (page)`);
1050
- text.setText(content);
1051
- return text;
1148
+ return new CollapsedText(content, "", theme.fg("muted", "..."));
1052
1149
  },
1053
1150
 
1054
1151
  renderResult(result, options, theme, context) {
1055
- return renderTextResult(result, options, theme, context, 15);
1152
+ return renderCompactTextResult(result, options, theme, context);
1056
1153
  },
1057
1154
  });
1058
1155
 
@@ -1179,23 +1276,21 @@ export default function fffExtension(pi: ExtensionAPI) {
1179
1276
  },
1180
1277
 
1181
1278
  renderCall(args, theme, context) {
1182
- const text = (context.lastComponent as Text | undefined) ?? new Text("", 0, 0);
1183
1279
  const pattern = args?.pattern ?? "";
1184
1280
  const path = args?.path ?? ".";
1185
1281
  let content =
1186
- theme.fg("toolTitle", theme.bold(toolNames.find)) +
1282
+ theme.fg("toolTitle", theme.bold(getRenderToolName(context, toolNames.find))) +
1187
1283
  " " +
1188
1284
  theme.fg("accent", pattern) +
1189
1285
  theme.fg("toolOutput", ` in ${path}`);
1190
1286
  if (args?.limit !== undefined)
1191
1287
  content += theme.fg("toolOutput", ` (limit ${args.limit})`);
1192
1288
  if (args?.cursor) content += theme.fg("muted", ` (page)`);
1193
- text.setText(content);
1194
- return text;
1289
+ return new CollapsedText(content, "", theme.fg("muted", "..."));
1195
1290
  },
1196
1291
 
1197
1292
  renderResult(result, options, theme, context) {
1198
- return renderTextResult(result, options, theme, context, 20);
1293
+ return renderCompactTextResult(result, options, theme, context);
1199
1294
  },
1200
1295
  });
1201
1296
 
@@ -1287,7 +1382,10 @@ export default function fffExtension(pi: ExtensionAPI) {
1287
1382
  const patterns = args?.patterns ?? [];
1288
1383
  const constraints = args?.constraints;
1289
1384
  let content =
1290
- theme.fg("toolTitle", theme.bold(toolNames.multiGrep)) +
1385
+ theme.fg(
1386
+ "toolTitle",
1387
+ theme.bold(getRenderToolName(context, toolNames.multiGrep)),
1388
+ ) +
1291
1389
  " " +
1292
1390
  theme.fg("accent", patterns.map((p: string) => `"${p}"`).join(", "));
1293
1391
  if (constraints) content += theme.fg("toolOutput", ` (${constraints})`);
@@ -1297,7 +1395,7 @@ export default function fffExtension(pi: ExtensionAPI) {
1297
1395
  },
1298
1396
 
1299
1397
  renderResult(result, options, theme, context) {
1300
- return renderTextResult(result, options, theme, context, 15);
1398
+ return renderPreviewResult(result, options, theme, context, 15);
1301
1399
  },
1302
1400
  });
1303
1401
  } // end if (enableMultiGrep)