pi-open-tui 0.4.2 → 0.4.3

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.
@@ -1,8 +1,8 @@
1
1
  import { homedir } from "node:os";
2
2
  import { isAbsolute, relative, sep } from "node:path";
3
- import type { ExtensionAPI, Theme, ThemeColor, ToolRenderers } from "@earendil-works/pi-coding-agent";
4
- import { Box, type Component, Spacer, truncateToWidth, visibleWidth } from "@earendil-works/pi-tui";
5
- import type { ToolViewMode } from "./config.ts";
3
+ import type { ExtensionAPI, SourceInfo, Theme, ThemeColor, ToolRenderers } from "@earendil-works/pi-coding-agent";
4
+ import { Box, type Component, getCapabilities, getImageDimensions, imageFallback, Spacer, Text, truncateToWidth, visibleWidth } from "@earendil-works/pi-tui";
5
+ import type { ToolViewConfig } from "./config.ts";
6
6
  import type { IconGlyphs } from "./icons.ts";
7
7
  import { codeLines, prepareToolPreview } from "./tool-preview.ts";
8
8
  import { sanitizeStatus, stripAnsi } from "./utils.ts";
@@ -24,6 +24,15 @@ const TOOL_LABELS: Record<string, string> = {
24
24
  ls: "LS",
25
25
  };
26
26
  const LIVE_TAIL_LINES = 3;
27
+ /** Arguments that best identify a call to a tool this view knows nothing else about. */
28
+ const GENERIC_DETAIL_KEYS = ["command", "query", "sql", "pattern", "prompt", "task", "url", "path", "file_path"];
29
+ const GENERIC_DETAIL_ARGS = 3;
30
+ const GENERIC_DETAIL_VALUE_CHARS = 80;
31
+ const FFF_TOOLS = new Map<string, "grep" | "find">([
32
+ ["ffgrep", "grep"], ["grep", "grep"],
33
+ ["fffind", "find"], ["find", "find"],
34
+ ["fff-multi-grep", "grep"], ["multi_grep", "grep"],
35
+ ]);
27
36
 
28
37
  /** Per-row slots: Pi's native renderers keep their own state and components here. */
29
38
  interface RowState {
@@ -33,6 +42,7 @@ interface RowState {
33
42
  callCard?: Box;
34
43
  callCardBottom?: Spacer;
35
44
  resultCard?: Box;
45
+ hasResult?: boolean;
36
46
  startedAt?: number;
37
47
  pulse?: ReturnType<typeof setInterval>;
38
48
  pulseBright?: boolean;
@@ -165,6 +175,23 @@ export function callDetail(toolName: string, args: Args, cwd: string): string {
165
175
  }
166
176
  }
167
177
 
178
+ /** `Tool(...)` for tools with unknown arguments: the most identifying one, else the first few as `key=value`. */
179
+ export function genericDetail(args: Args, cwd: string): string {
180
+ for (const key of GENERIC_DETAIL_KEYS) {
181
+ const value = args[key];
182
+ if (typeof value !== "string" || !value.trim()) continue;
183
+ return key === "path" || key === "file_path" ? displayPath(value, cwd) : firstLine(value);
184
+ }
185
+ return Object.entries(args)
186
+ .filter(([, value]) => value !== undefined && value !== null)
187
+ .slice(0, GENERIC_DETAIL_ARGS)
188
+ .map(([key, value]) => {
189
+ const shown = typeof value === "string" ? firstLine(value) : JSON.stringify(value) ?? String(value);
190
+ return `${key}=${shown.slice(0, GENERIC_DETAIL_VALUE_CHARS)}`;
191
+ })
192
+ .join(", ");
193
+ }
194
+
168
195
  function details(result: ToolResult): Record<string, unknown> {
169
196
  const value: unknown = result.details;
170
197
  return typeof value === "object" && value !== null ? (value as Record<string, unknown>) : {};
@@ -235,6 +262,77 @@ export function summarizeResult(toolName: string, result: ToolResult, args: Args
235
262
  }
236
263
  }
237
264
 
265
+ /**
266
+ * One-line summary for a tool whose output format is unknown, so nothing is parsed out of it.
267
+ * The first line goes last so narrow rows cut it before the counts and duration.
268
+ */
269
+ export function summarizeGeneric(result: ToolResult, duration = ""): string {
270
+ const body = textOutput(result).split("\n").filter((line) => line.trim());
271
+ const images = result.content.filter((block) => block.type === "image").length;
272
+ const head = sanitizeStatus((body[0] ?? "").replace(/^#+\s+/, ""));
273
+ const info = truncation(result);
274
+ const totalLines = info.truncated && typeof info.totalLines === "number"
275
+ && Number.isSafeInteger(info.totalLines) && info.totalLines >= body.length
276
+ ? info.totalLines : body.length;
277
+ const counts = [
278
+ ...(totalLines ? [plural(totalLines, "line")] : []),
279
+ ...(images ? [plural(images, "image")] : []),
280
+ ].join(" · ");
281
+ return (counts || "Done") + (info.truncated ? " · truncated" : "") + duration + (head ? ` · ${head}` : "");
282
+ }
283
+
284
+ /** FFF's grep count belongs to this page; find's count covers all matching files. */
285
+ function summarizeFff(kind: "grep" | "find", result: ToolResult, duration = ""): string {
286
+ const info = details(result);
287
+ const matched = info.totalMatched;
288
+ if (typeof matched !== "number" || !Number.isSafeInteger(matched) || matched < 0
289
+ || result.content.some((block) => block.type === "image")) return summarizeGeneric(result, duration);
290
+ const output = textOutput(result).trim();
291
+ const footer = /\n\n\[(Invalid regex: [\s\S]+, used literal match(?:\. Continue with cursor="fff_c\d+")?|[^\n]+)\]$/.exec(output);
292
+ const body = footer ? output.slice(0, footer.index) : output;
293
+ // FFF also returns zero counts for its wildcard guard; keep that explanation first.
294
+ if (matched === 0 && body !== (kind === "grep" ? "No matches found" : "No files found matching pattern")) {
295
+ return summarizeGeneric(result, duration);
296
+ }
297
+ const notice = footer?.[1] ?? "";
298
+ const fuzzy = kind === "grep" && output.startsWith("[0 exact matches. Maybe you meant this?]\n");
299
+ const literal = kind === "grep" && /^Invalid regex: [\s\S]+, used literal match(?:\. Continue with cursor="fff_c\d+")?$/.test(notice);
300
+ let more: boolean;
301
+ let weak: RegExpExecArray | null = null;
302
+ if (kind === "find") {
303
+ if (typeof info.hasMore !== "boolean" || typeof info.pageIndex !== "number"
304
+ || !Number.isSafeInteger(info.pageIndex) || info.pageIndex < 0) return summarizeGeneric(result, duration);
305
+ // Weak fuzzy results can have hasMore=true while FFF intentionally provides no cursor.
306
+ weak = /^Query "[^\n]*" produced only weak scattered fuzzy matches\. Output capped at (\d+)\/(\d+)\.$/.exec(notice);
307
+ if (weak && (Number(weak[2]) !== matched || Number(weak[1]) > matched)) return summarizeGeneric(result, duration);
308
+ more = info.hasMore && /^\d+ more match(?:es)? available\. cursor="\d+" to continue$/.test(notice);
309
+ if (info.hasMore && !more && !weak) return summarizeGeneric(result, duration);
310
+ } else {
311
+ more = /(?:^|\. )(?:Continue with cursor="fff_c\d+"|More available\. cursor="fff_c\d+" to continue)$/.test(notice);
312
+ }
313
+ const matchType = fuzzy ? "fuzzy " : literal ? "literal " : "";
314
+ const count = kind === "find" ? `${plural(matched, "file")} matched`
315
+ : `${plural(matched, `${matchType}match`, `${matchType}matches`)} on this page`;
316
+ const head = sanitizeStatus(output.split("\n")[fuzzy ? 1 : 0] ?? "");
317
+ return count + (more ? " · more available" : "")
318
+ + (literal ? " · regex fallback" : "")
319
+ + (weak ? " · weak matches" + (Number(weak[1]) < matched ? " · capped" : "") : "")
320
+ + (truncation(result).truncated ? " · truncated" : "")
321
+ + duration + (head ? ` · ${head}` : "");
322
+ }
323
+
324
+ /** Match npm provenance, its installed entry point, or the official git source and entry point. */
325
+ export function fffKind(toolName: string, sourceInfo: SourceInfo | undefined): "grep" | "find" | undefined {
326
+ const kind = FFF_TOOLS.get(toolName);
327
+ if (!kind || !sourceInfo) return undefined;
328
+ const source = sourceInfo.source;
329
+ const path = sourceInfo.path.replace(/\\/g, "/");
330
+ const git = /^(?:git:)?(?:https?:\/\/github\.com\/|ssh:\/\/git@github\.com\/|git@github\.com:|github\.com\/)dmtrkovalenko\/fff(?:\.git)?(?:[@#][^\s]+)?$/i.test(source)
331
+ && /(?:^|\/)packages\/pi-fff\/src\/index\.ts$/.test(path);
332
+ return source === "npm:@ff-labs/pi-fff" || source.startsWith("npm:@ff-labs/pi-fff@")
333
+ || /(?:^|\/)node_modules\/@ff-labs\/pi-fff\/src\/index\.ts$/.test(path) || git ? kind : undefined;
334
+ }
335
+
238
336
  /**
239
337
  * One-line summary for a failed call: the shell's closing status plus its last output line,
240
338
  * otherwise the error's first line.
@@ -300,24 +398,58 @@ function stopPulse(row: RowState): void {
300
398
  row.pulse = undefined;
301
399
  }
302
400
 
401
+ /** Pi's expanded card for a tool without its own call renderer. */
402
+ function genericCall(toolName: string, args: unknown, theme: Theme): Component {
403
+ const entries = typeof args === "object" && args !== null ? Object.entries(args) : [];
404
+ const rows = entries.map(([key, value]) => {
405
+ const shown = typeof value === "string" ? value : JSON.stringify(value, null, 2) ?? String(value);
406
+ return ` ${sanitizeStatus(key)}: ${cleanOutput(shown).replace(/\t/g, " ").split("\n").join("\n ")}`;
407
+ });
408
+ const title = theme.fg("toolTitle", theme.bold(sanitizeStatus(toolName)));
409
+ return new Text(rows.length ? `${title}\n${theme.fg("muted", rows.join("\n"))}` : title, 0, 0);
410
+ }
411
+
412
+ /** Pi's expanded card for a tool without its own result renderer. Pi still draws images it can show. */
413
+ function genericResult(result: ToolResult, theme: Theme, context: RenderContext): Component {
414
+ const inline = getCapabilities().images && context.showImages;
415
+ const missing = inline ? [] : result.content.flatMap((block) => (block.type === "image" ? [imageFallback(block.mimeType, getImageDimensions(block.data, block.mimeType) ?? undefined)] : []));
416
+ const output = [textOutput(result).replace(/\t/g, " "), ...missing].filter(Boolean).join("\n");
417
+ return new Text(output ? theme.fg("toolOutput", output) : "", 0, 0);
418
+ }
419
+
420
+ /**
421
+ * `generic` is for tools other than Pi's own built-ins: their output format and renderers are
422
+ * unknown. A known extension can supply its own summary. Native renderers start on expansion;
423
+ * after collapse they still receive completion and error updates, but skip hidden progress.
424
+ */
303
425
  export function createCompactRenderers(
304
426
  toolName: string,
305
- native: ToolRenderers,
427
+ native: ToolRenderers | undefined,
306
428
  glyphs: () => IconGlyphs,
307
429
  running?: Map<string, RowState | undefined>,
430
+ generic = false,
431
+ summarizeExtension = summarizeGeneric,
308
432
  ): ToolRenderers {
309
- const label = TOOL_LABELS[toolName] ?? toolName;
310
- const isShell = toolName === "bash" || toolName === "powershell";
433
+ const label = Object.hasOwn(TOOL_LABELS, toolName) ? TOOL_LABELS[toolName]! : sanitizeStatus(toolName);
434
+ const isShell = !generic && (toolName === "bash" || toolName === "powershell");
435
+ const nativeShell = native?.renderShell;
436
+ const renderNativeCall: RenderCall | undefined = native?.renderCall
437
+ ?? (generic ? (args, theme) => genericCall(toolName, args, theme) : undefined);
438
+ const renderNativeResult: RenderResult | undefined = native?.renderResult
439
+ ?? (generic ? (result, _options, theme, context) => genericResult(result, theme, context) : undefined);
311
440
  // Expanded rows hand off to Pi's own card so nothing is hidden.
312
441
  const showNative = (context: RenderContext) => context.expanded;
442
+ // A generic row that was expanded skips hidden progress but still sees the call finish, for a renderer's own timers.
443
+ const feedCollapsedNative = (row: RowState, isPartial: boolean, isError: boolean) =>
444
+ row.nativeCall !== undefined && (!isPartial || isError);
313
445
 
314
446
  return {
315
447
  renderShell: "self",
316
448
  renderCall(args, theme, context) {
317
449
  const row = rowState(context);
318
450
  // Keep native timers/edit state current; write rebuilds its full highlight cache only on expansion.
319
- if (toolName !== "write" || context.expanded) {
320
- row.nativeCall = native.renderCall?.(args, theme, nativeContext(context, row, row.nativeCall));
451
+ if (context.expanded || (generic ? feedCollapsedNative(row, context.isPartial, context.isError) : toolName !== "write")) {
452
+ row.nativeCall = renderNativeCall?.(args, theme, nativeContext(context, row, row.nativeCall));
321
453
  }
322
454
  if (context.executionStarted) row.startedAt ??= Date.now();
323
455
  const live = context.executionStarted && context.isPartial && !context.isError;
@@ -336,7 +468,7 @@ export function createCompactRenderers(
336
468
  stopPulse(row);
337
469
  }
338
470
  if (showNative(context) && row.nativeCall) {
339
- if (native.renderShell === "self") return row.nativeCall;
471
+ if (nativeShell === "self") return row.nativeCall;
340
472
  // Recreate Pi's default card: top padding, call, then result (or bottom padding while pending).
341
473
  row.callCard ??= new Box(context.outputPad, 0);
342
474
  row.callCardBottom ??= new Spacer(1);
@@ -352,20 +484,33 @@ export function createCompactRenderers(
352
484
  ? live && row.pulseBright !== false ? "accent" : "dim"
353
485
  : "success";
354
486
  const titleColor = context.isPartial || context.isError ? "toolTitle" : "dim";
355
- const detail = sanitizeStatus(callDetail(toolName, (args ?? {}) as Args, context.cwd));
487
+ const callArgs = (args ?? {}) as Args;
488
+ const detail = sanitizeStatus(generic ? genericDetail(callArgs, context.cwd) : callDetail(toolName, callArgs, context.cwd));
356
489
  const pad = " ".repeat(context.outputPad);
357
- return lines((_width, icons) => [
358
- pad + theme.fg(color, icons.toolCall) + " " + theme.fg(titleColor, titleColor === "dim" ? label : theme.bold(label))
359
- + (detail ? theme.fg(context.isPartial || context.isError ? "muted" : "dim", `(${detail})`) : ""),
360
- ], theme, glyphs, !context.isPartial);
490
+ return lines((_width, icons) => {
491
+ const call = pad + theme.fg(color, icons.toolCall) + " " + theme.fg(titleColor, titleColor === "dim" ? label : theme.bold(label))
492
+ + (detail ? theme.fg(context.isPartial || context.isError ? "muted" : "dim", `(${detail})`) : "");
493
+ // Pi does not call renderResult until the tool provides its first result.
494
+ // Read the shared flag during rendering, after both slots have been prepared.
495
+ if (generic && live && !row.hasResult) {
496
+ const elapsed = row.startedAt === undefined ? "" : ` ${formatElapsed(Date.now() - row.startedAt)}`;
497
+ const gutter = `${pad} ${theme.fg("dim", icons.toolResult)} `;
498
+ return [call, gutter + theme.fg("warning", `Running…${elapsed}`)];
499
+ }
500
+ return [call];
501
+ }, theme, glyphs, !context.isPartial);
361
502
  },
362
503
 
363
504
  renderResult(result, options, theme, context) {
364
505
  const row = rowState(context);
506
+ row.hasResult = true;
507
+ const partial = options.isPartial && !context.isError;
365
508
  if (!options.isPartial || context.isError) stopPulse(row);
366
- row.nativeResult = native.renderResult?.(result, options, theme, nativeContext(context, row, row.nativeResult));
509
+ if (!generic || context.expanded || feedCollapsedNative(row, options.isPartial, context.isError)) {
510
+ row.nativeResult = renderNativeResult?.(result, options, theme, nativeContext(context, row, row.nativeResult));
511
+ }
367
512
  if (showNative(context) && row.nativeResult) {
368
- if (native.renderShell === "self") return row.nativeResult;
513
+ if (nativeShell === "self") return row.nativeResult;
369
514
  // The result now closes the card, so the call card drops its pending bottom padding.
370
515
  if (row.callCard && row.callCardBottom) row.callCard.removeChild(row.callCardBottom);
371
516
  row.resultCard ??= new Box(context.outputPad, 0);
@@ -378,30 +523,31 @@ export function createCompactRenderers(
378
523
  }
379
524
  const pad = " ".repeat(context.outputPad);
380
525
  const args = (context.args ?? {}) as Args;
381
- if (!options.isPartial && !context.isError && (toolName === "edit" || toolName === "write")) {
526
+ if (!generic && !options.isPartial && !context.isError && (toolName === "edit" || toolName === "write")) {
382
527
  const source = toolName === "edit" ? str(details(result).diff) : str(args.content);
383
528
  const path = str(args.file_path ?? args.path);
384
529
  if (!row.preview || row.preview.source !== source || row.preview.path !== path || row.preview.colors !== theme.colors) {
385
530
  row.preview = { source, path, colors: theme.colors, render: prepareToolPreview(toolName, source, path, theme) };
386
531
  }
387
532
  }
388
- const duration = isShell && context.durationMs !== undefined ? ` · ${formatElapsed(context.durationMs)}` : "";
389
- const summary = options.isPartial ? "" : context.isError
390
- ? summarizeError(toolName, result, duration)
391
- : summarizeResult(toolName, result, args) + duration;
392
- const tail = options.isPartial && isShell ? tailLines(textOutput(result), LIVE_TAIL_LINES) : [];
533
+ const timed = isShell || generic;
534
+ const duration = timed && context.durationMs !== undefined ? ` · ${formatElapsed(context.durationMs)}` : "";
535
+ const summary = partial ? "" : context.isError
536
+ ? summarizeError(generic ? "" : toolName, result, duration)
537
+ : generic ? summarizeExtension(result, duration) : summarizeResult(toolName, result, args) + duration;
538
+ const tail = partial && isShell ? tailLines(textOutput(result), LIVE_TAIL_LINES) : [];
393
539
  return lines((width, { toolResult }) => {
394
540
  const gutter = `${pad} ${theme.fg("dim", toolResult)} `;
395
541
  const indent = `${pad} ${" ".repeat(visibleWidth(toolResult))} `;
396
- if (options.isPartial) {
397
- if (!isShell) return [gutter + theme.fg("warning", "Running…")];
542
+ if (partial) {
543
+ if (!timed) return [gutter + theme.fg("warning", "Running…")];
398
544
  const elapsed = row.startedAt === undefined ? "" : ` ${formatElapsed(Date.now() - row.startedAt)}`;
399
545
  return [...tail.map((line) => theme.fg("toolOutput", line)), theme.fg("warning", `Running…${elapsed}`)]
400
546
  .map((line, index) => (index === 0 ? gutter : indent) + line);
401
547
  }
402
548
  if (context.isError) return [gutter + theme.fg("error", summary)];
403
549
  return [gutter + theme.fg("dim", summary), ...(row.preview?.render(width, indent) ?? [])];
404
- }, theme, glyphs, !options.isPartial);
550
+ }, theme, glyphs, !partial);
405
551
  },
406
552
  };
407
553
  }
@@ -409,10 +555,10 @@ export function createCompactRenderers(
409
555
  const OWNED_TOOLS = new Set(Object.keys(TOOL_LABELS));
410
556
 
411
557
  /**
412
- * Display-only: Pi's tools, execution, arguments and results are untouched. The mode is read
413
- * when Pi builds a row, so switching it affects new rows only.
558
+ * Display-only: Pi's tools, execution, arguments and results are untouched. The configuration is
559
+ * read when Pi builds a row, so changing it affects new rows only.
414
560
  */
415
- export function registerToolView(pi: ExtensionAPI, getMode: () => ToolViewMode, glyphs: () => IconGlyphs): void {
561
+ export function registerToolView(pi: ExtensionAPI, getView: () => ToolViewConfig, glyphs: () => IconGlyphs): void {
416
562
  const running = new Map<string, RowState | undefined>();
417
563
  pi.on("tool_execution_start", (event) => { running.set(event.toolCallId, undefined); });
418
564
  pi.on("tool_execution_end", (event) => {
@@ -426,8 +572,15 @@ export function registerToolView(pi: ExtensionAPI, getMode: () => ToolViewMode,
426
572
  });
427
573
  pi.registerToolRenderer((toolName, next) => {
428
574
  const native = next();
429
- if (!native || !OWNED_TOOLS.has(toolName) || getMode() !== "compact") return native;
430
- if (pi.getAllTools().find((tool) => tool.name === toolName)?.sourceInfo?.source !== "builtin") return native;
431
- return createCompactRenderers(toolName, native, glyphs, running);
575
+ const view = getView();
576
+ if (view.mode !== "compact" || view.exclude.includes(toolName)) return native;
577
+ const sourceInfo = OWNED_TOOLS.has(toolName) || view.extensionTools && FFF_TOOLS.has(toolName)
578
+ ? pi.getAllTools().find((tool) => tool.name === toolName)?.sourceInfo : undefined;
579
+ const builtin = OWNED_TOOLS.has(toolName) && sourceInfo?.source === "builtin";
580
+ if (builtin) return native && createCompactRenderers(toolName, native, glyphs, running);
581
+ if (!view.extensionTools) return native;
582
+ const fff = fffKind(toolName, sourceInfo);
583
+ return createCompactRenderers(toolName, native, glyphs, running, true,
584
+ fff ? (result, duration) => summarizeFff(fff, result, duration) : summarizeGeneric);
432
585
  });
433
586
  }
@@ -133,6 +133,13 @@ export type PrioritizedSegment = {
133
133
  truncate?: (text: string, maxWidth: number, ellipsis: string) => string;
134
134
  };
135
135
 
136
+ export interface SegmentFitOptions {
137
+ /** Gap between adjacent surviving segments, using their original indices. */
138
+ gapWidth?: (previousIndex: number, nextIndex: number) => number;
139
+ /** Keep existing callers' compact order unless the row opts into priorities. */
140
+ compactOrder?: "input" | "priority";
141
+ }
142
+
136
143
  /**
137
144
  * Pack segments into maxWidth: compact segments first, then shrink/drop the
138
145
  * lowest-priority segments (higher priority = survives longer). Returns one
@@ -142,8 +149,10 @@ export function fitSegmentsByPriority(
142
149
  segs: readonly PrioritizedSegment[],
143
150
  maxW: number,
144
151
  ellipsis = "...",
152
+ options: SegmentFitOptions = {},
145
153
  ): string[] {
146
- const items = segs.map((s) => ({
154
+ const items = segs.map((s, index) => ({
155
+ index,
147
156
  text: s.text,
148
157
  compactText: s.compactText,
149
158
  priority: s.priority,
@@ -152,11 +161,16 @@ export function fitSegmentsByPriority(
152
161
  }));
153
162
  const totalW = () => {
154
163
  const active = items.filter((it) => it.text !== "");
155
- return active.reduce((a, it) => a + it.w, 0) + Math.max(0, active.length - 1);
164
+ return active.reduce((width, item, index) => width + item.w + (index === 0
165
+ ? 0
166
+ : options.gapWidth?.(active[index - 1]!.index, item.index) ?? 1), 0);
156
167
  };
157
168
  // Compact (e.g. cwd -> basename) before sacrificing any segment content.
158
169
  if (totalW() > maxW) {
159
- for (const item of items) {
170
+ const compactItems = options.compactOrder === "priority"
171
+ ? [...items].sort((a, b) => a.priority - b.priority || a.index - b.index)
172
+ : items;
173
+ for (const item of compactItems) {
160
174
  if (!item.compactText || visibleWidth(item.compactText) >= item.w) continue;
161
175
  item.text = item.compactText;
162
176
  item.w = visibleWidth(item.text);
@@ -189,9 +203,7 @@ export function fitSegmentsByPriority(
189
203
  }
190
204
  }
191
205
  if (target === -1) break;
192
- const others = items.filter((_, i) => i !== target && items[i].text !== "");
193
- const otherW = others.reduce((a, it) => a + it.w, 0) + Math.max(0, others.length - 1);
194
- const avail = maxW - otherW - (others.length > 0 ? 1 : 0);
206
+ const avail = maxW - (nowTotal - items[target].w);
195
207
  if (avail <= visibleWidth(ellipsis)) {
196
208
  items[target].text = "";
197
209
  items[target].w = 0;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-open-tui",
3
- "version": "0.4.2",
3
+ "version": "0.4.3",
4
4
  "description": "A polished TUI for Pi coding agent: branded header, responsive footer, framed editor, thinking preview, and compact tool views.",
5
5
  "type": "module",
6
6
  "repository": {
@@ -19,6 +19,7 @@
19
19
  "license": "MIT",
20
20
  "files": [
21
21
  "extensions/",
22
+ "docs/",
22
23
  "assets/",
23
24
  "CHANGELOG.md",
24
25
  "README.md",
@@ -26,7 +27,7 @@
26
27
  "LICENSE"
27
28
  ],
28
29
  "scripts": {
29
- "test": "node --test tests/settings-command.test.ts tests/footer.test.ts tests/editor.test.ts tests/telemetry.test.ts tests/peek.test.ts tests/tool-view.test.ts tests/copy-buttons.test.ts",
30
+ "test": "node --test tests/settings-command.test.ts tests/footer.test.ts tests/footer-protocol.test.ts tests/footer-segments.test.ts tests/editor.test.ts tests/telemetry.test.ts tests/peek.test.ts tests/tool-view.test.ts tests/copy-buttons.test.ts tests/copy-markdown.test.ts",
30
31
  "typecheck": "tsc --noEmit"
31
32
  },
32
33
  "pi": {
@@ -36,9 +37,9 @@
36
37
  "image": "https://raw.githubusercontent.com/OldSuns/pi-open-tui/main/assets/preview_dashboard_1.png"
37
38
  },
38
39
  "peerDependencies": {
39
- "@earendil-works/pi-ai": ">=1.2.0",
40
- "@earendil-works/pi-coding-agent": ">=1.2.0",
41
- "@earendil-works/pi-tui": ">=1.2.0"
40
+ "@earendil-works/pi-ai": ">=1.5.0",
41
+ "@earendil-works/pi-coding-agent": ">=1.5.0",
42
+ "@earendil-works/pi-tui": ">=1.5.0"
42
43
  },
43
44
  "peerDependenciesMeta": {
44
45
  "@earendil-works/pi-coding-agent": {
@@ -52,9 +53,9 @@
52
53
  }
53
54
  },
54
55
  "devDependencies": {
55
- "@earendil-works/pi-ai": "^1.2.0",
56
- "@earendil-works/pi-coding-agent": "^1.2.0",
57
- "@earendil-works/pi-tui": "^1.2.0",
56
+ "@earendil-works/pi-ai": "^1.5.0",
57
+ "@earendil-works/pi-coding-agent": "^1.5.0",
58
+ "@earendil-works/pi-tui": "^1.5.0",
58
59
  "@types/node": "^22.0.0",
59
60
  "typescript": "^5.5.0"
60
61
  },