@oh-my-pi/pi-coding-agent 16.1.6 → 16.1.8

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 (97) hide show
  1. package/CHANGELOG.md +52 -0
  2. package/dist/cli.js +3160 -3139
  3. package/dist/types/cli/session-picker.d.ts +0 -1
  4. package/dist/types/cli/tiny-models-cli.d.ts +2 -0
  5. package/dist/types/collab/protocol.d.ts +20 -1
  6. package/dist/types/config/models-config-schema.d.ts +10 -0
  7. package/dist/types/config/models-config.d.ts +6 -0
  8. package/dist/types/config/settings-schema.d.ts +57 -6
  9. package/dist/types/extensibility/legacy-pi-ai-shim.d.ts +5 -1
  10. package/dist/types/internal-urls/filesystem-resource.d.ts +9 -0
  11. package/dist/types/mcp/loader.d.ts +3 -2
  12. package/dist/types/mcp/manager.d.ts +4 -3
  13. package/dist/types/mcp/startup-events.d.ts +21 -4
  14. package/dist/types/mnemopi/config.d.ts +1 -0
  15. package/dist/types/modes/components/agent-hub.d.ts +1 -0
  16. package/dist/types/modes/components/agent-transcript-viewer.d.ts +1 -0
  17. package/dist/types/modes/components/assistant-message.d.ts +5 -1
  18. package/dist/types/modes/components/btw-panel.d.ts +2 -0
  19. package/dist/types/modes/components/chat-transcript-builder.d.ts +1 -0
  20. package/dist/types/modes/components/session-selector.d.ts +0 -2
  21. package/dist/types/modes/controllers/btw-controller.d.ts +2 -0
  22. package/dist/types/modes/controllers/selector-controller.d.ts +1 -0
  23. package/dist/types/modes/controllers/streaming-reveal.d.ts +3 -2
  24. package/dist/types/modes/interactive-mode.d.ts +3 -0
  25. package/dist/types/modes/types.d.ts +3 -0
  26. package/dist/types/session/agent-session.d.ts +5 -3
  27. package/dist/types/session/session-history-format.d.ts +25 -0
  28. package/dist/types/session/snapcompact-inline.d.ts +1 -1
  29. package/dist/types/slash-commands/builtin-registry.d.ts +7 -4
  30. package/dist/types/slash-commands/types.d.ts +2 -0
  31. package/dist/types/system-prompt.d.ts +1 -0
  32. package/dist/types/tiny/models.d.ts +11 -7
  33. package/dist/types/tools/todo.d.ts +1 -0
  34. package/dist/types/utils/thinking-display.d.ts +2 -0
  35. package/package.json +12 -12
  36. package/src/advisor/__tests__/advisor.test.ts +104 -0
  37. package/src/advisor/runtime.ts +38 -2
  38. package/src/cli/session-picker.ts +1 -2
  39. package/src/cli/tiny-models-cli.ts +7 -2
  40. package/src/collab/guest.ts +172 -20
  41. package/src/collab/host.ts +47 -5
  42. package/src/collab/protocol.ts +16 -1
  43. package/src/config/models-config-schema.ts +1 -0
  44. package/src/config/settings-schema.ts +59 -5
  45. package/src/edit/renderer.ts +8 -12
  46. package/src/extensibility/legacy-pi-ai-shim.ts +16 -4
  47. package/src/internal-urls/docs-index.generated.txt +1 -1
  48. package/src/internal-urls/filesystem-resource.ts +34 -0
  49. package/src/internal-urls/local-protocol.ts +7 -1
  50. package/src/internal-urls/memory-protocol.ts +5 -1
  51. package/src/internal-urls/skill-protocol.ts +20 -4
  52. package/src/internal-urls/vault-protocol.ts +5 -2
  53. package/src/main.ts +8 -8
  54. package/src/mcp/loader.ts +4 -3
  55. package/src/mcp/manager.ts +35 -15
  56. package/src/mcp/startup-events.ts +106 -11
  57. package/src/mnemopi/config.ts +2 -0
  58. package/src/mnemopi/state.ts +3 -1
  59. package/src/modes/components/agent-hub.ts +4 -0
  60. package/src/modes/components/agent-transcript-viewer.ts +2 -0
  61. package/src/modes/components/assistant-message.ts +217 -18
  62. package/src/modes/components/btw-panel.ts +15 -3
  63. package/src/modes/components/chat-transcript-builder.ts +8 -2
  64. package/src/modes/components/model-selector.ts +72 -9
  65. package/src/modes/components/omfg-panel.ts +1 -1
  66. package/src/modes/components/session-selector.ts +4 -9
  67. package/src/modes/components/snapcompact-shape-preview.ts +1 -1
  68. package/src/modes/components/tool-execution.ts +10 -2
  69. package/src/modes/controllers/btw-controller.ts +32 -7
  70. package/src/modes/controllers/event-controller.ts +24 -4
  71. package/src/modes/controllers/input-controller.ts +43 -21
  72. package/src/modes/controllers/selector-controller.ts +23 -13
  73. package/src/modes/controllers/streaming-reveal.ts +78 -20
  74. package/src/modes/controllers/todo-command-controller.ts +9 -10
  75. package/src/modes/interactive-mode.ts +83 -8
  76. package/src/modes/types.ts +3 -0
  77. package/src/modes/utils/ui-helpers.ts +1 -0
  78. package/src/prompts/advisor/system.md +1 -1
  79. package/src/prompts/system/side-channel-no-tools.md +3 -0
  80. package/src/prompts/system/system-prompt.md +164 -156
  81. package/src/sdk.ts +12 -8
  82. package/src/session/agent-session.ts +349 -84
  83. package/src/session/history-storage.ts +15 -16
  84. package/src/session/session-history-format.ts +41 -1
  85. package/src/session/snapcompact-inline.ts +9 -6
  86. package/src/slash-commands/builtin-registry.ts +147 -21
  87. package/src/slash-commands/helpers/todo.ts +12 -6
  88. package/src/slash-commands/types.ts +2 -0
  89. package/src/system-prompt.ts +6 -11
  90. package/src/tiny/models.ts +7 -3
  91. package/src/tiny/title-client.ts +8 -2
  92. package/src/tiny/worker.ts +1 -0
  93. package/src/tools/search.ts +10 -1
  94. package/src/tools/sqlite-reader.ts +59 -3
  95. package/src/tools/todo.ts +6 -0
  96. package/src/tools/write.ts +4 -6
  97. package/src/utils/thinking-display.ts +78 -0
@@ -296,6 +296,7 @@ export class TinyTitleClient {
296
296
  #unsubscribeMessage: (() => void) | null = null;
297
297
  #unsubscribeError: (() => void) | null = null;
298
298
  #pending = new Map<string, PendingRequest>();
299
+ #failedModels = new Set<TinyLocalModelKey>();
299
300
  #progressListeners = new Set<(event: TinyTitleProgressEvent) => void>();
300
301
  #nextRequestId = 0;
301
302
  #spawnWorker: () => WorkerHandle;
@@ -318,7 +319,7 @@ export class TinyTitleClient {
318
319
  ): Promise<string | null> {
319
320
  const options = normalizeTinyTitleGenerateOptions(optionsOrSignal);
320
321
  if (!isTinyTitleLocalModelKey(modelKey)) return null;
321
- if (options.signal?.aborted) return null;
322
+ if (options.signal?.aborted || this.#failedModels.has(modelKey)) return null;
322
323
 
323
324
  try {
324
325
  const worker = this.#ensureWorker();
@@ -357,7 +358,7 @@ export class TinyTitleClient {
357
358
  options: { maxTokens?: number; signal?: AbortSignal } = {},
358
359
  ): Promise<string | null> {
359
360
  if (!isTinyMemoryLocalModelKey(modelKey)) return null;
360
- if (options.signal?.aborted) return null;
361
+ if (options.signal?.aborted || this.#failedModels.has(modelKey)) return null;
361
362
 
362
363
  try {
363
364
  const worker = this.#ensureWorker();
@@ -478,12 +479,17 @@ export class TinyTitleClient {
478
479
  return;
479
480
  }
480
481
  logger.debug("tiny-title: worker returned error", { error: message.error });
482
+ this.#markFailedModel(pending);
481
483
  this.#emitProgress({ modelKey: pending.modelKey, status: "error" });
482
484
  if (pending.kind === "generate" || pending.kind === "complete") pending.resolve(null);
483
485
  else pending.resolve(false);
484
486
  void this.terminate();
485
487
  }
486
488
 
489
+ #markFailedModel(pending: PendingRequest): void {
490
+ if (pending.kind === "generate" || pending.kind === "complete") this.#failedModels.add(pending.modelKey);
491
+ }
492
+
487
493
  #emitProgress(event: TinyTitleProgressEvent): void {
488
494
  for (const listener of this.#progressListeners) listener(event);
489
495
  }
@@ -317,6 +317,7 @@ async function loadPipeline(
317
317
  ): Promise<TextGenerationPipeline> {
318
318
  const spec = getTinyLocalModelSpec(modelKey);
319
319
  if (!spec) throw new Error(`Unknown tiny local model: ${modelKey}`);
320
+ if (spec.unsupportedReason) throw new Error(`${modelKey} is unavailable: ${spec.unsupportedReason}`);
320
321
  const cached = pipelines.get(modelKey);
321
322
  if (cached) {
322
323
  void cached
@@ -278,6 +278,15 @@ interface InternalSearchInputResolution {
278
278
  virtualScopePath?: string;
279
279
  }
280
280
 
281
+ function isImmutableSourcePath(filePath: string, immutableSourcePaths: ReadonlySet<string>): boolean {
282
+ for (const immutablePath of immutableSourcePaths) {
283
+ if (filePath === immutablePath || filePath.startsWith(`${immutablePath}${path.sep}`)) {
284
+ return true;
285
+ }
286
+ }
287
+ return false;
288
+ }
289
+
281
290
  interface IndexedContentLines {
282
291
  lines: string[];
283
292
  starts: number[];
@@ -1131,7 +1140,7 @@ export class SearchTool implements AgentTool<typeof searchSchema, SearchToolDeta
1131
1140
  for (const relativePath of fileList) {
1132
1141
  if (archiveDisplaySet.has(relativePath) || virtualPathSet.has(relativePath)) continue;
1133
1142
  const absoluteFilePath = path.resolve(this.session.cwd, relativePath);
1134
- if (immutableSourcePaths.has(absoluteFilePath)) continue;
1143
+ if (isImmutableSourcePath(absoluteFilePath, immutableSourcePaths)) continue;
1135
1144
  // Mint a whole-file content tag so any anchor validates while the
1136
1145
  // file is unchanged; over-cap / unreadable files get no tag (and
1137
1146
  // therefore plain, non-editable line output).
@@ -21,7 +21,19 @@ const MAX_QUERY_LIMIT = 500;
21
21
  export const MAX_RAW_QUERY_ROWS = 1000;
22
22
  const MAX_RENDER_WIDTH = 120;
23
23
  const MAX_COLUMN_WIDTH = 40;
24
- const MIN_COLUMN_WIDTH = 1;
24
+ /**
25
+ * Floor for each ASCII-table column. At width 2 (or 1) every multi-char cell
26
+ * collapses to a lone ellipsis, so the renderer keeps each column wide enough
27
+ * to show at least one real glyph alongside the ellipsis (e.g. `Fo…`). When a
28
+ * row has too many columns to honor this floor inside `MAX_RENDER_WIDTH`,
29
+ * `buildAsciiTable` falls back to per-row vertical blocks via
30
+ * {@link buildVerticalBlocks} — issue #3107.
31
+ */
32
+ const MIN_COLUMN_WIDTH = 3;
33
+ /** Separator overhead per column in the ASCII table (`" | "`). */
34
+ const COLUMN_SEPARATOR_WIDTH = 3;
35
+ /** Constant frame overhead added once to every row (leading `"|"` + trailing `" |"` after the per-column accounting). */
36
+ const TABLE_FRAME_WIDTH = 1;
25
37
  /**
26
38
  * Upper bound on rows scanned when counting a table for the listing. SQLite has
27
39
  * no stored row count, so `COUNT(*)` is a full b-tree scan — multi-second on a
@@ -142,10 +154,53 @@ function padCell(value: string, width: number): string {
142
154
  return `${truncated}${" ".repeat(width - visibleWidth)}`;
143
155
  }
144
156
 
157
+ /**
158
+ * Width budget the ASCII layout needs at the floor (each column at
159
+ * `MIN_COLUMN_WIDTH`). When this exceeds `MAX_RENDER_WIDTH`, no choice of
160
+ * per-column widths can fit the header inside the budget — every cell is then
161
+ * forced down to width 1 by the shrink loop, rendering as a lone ellipsis, and
162
+ * the right edge is still chopped by the final per-line truncation (#3107).
163
+ */
164
+ function tableFitsAtMinimum(columnCount: number): boolean {
165
+ return MIN_COLUMN_WIDTH * columnCount + COLUMN_SEPARATOR_WIDTH * columnCount + TABLE_FRAME_WIDTH <= MAX_RENDER_WIDTH;
166
+ }
167
+
168
+ /**
169
+ * Vertical fallback used when a table has too many columns to fit horizontally
170
+ * (>19 at the default 120-cell budget). Each row becomes a labelled block of
171
+ * `column: value` lines, mirroring `psql`'s expanded display mode. Column
172
+ * names are right-padded so colons align; the value is left raw and the whole
173
+ * line is truncated at `MAX_RENDER_WIDTH`.
174
+ */
175
+ function buildVerticalBlocks(columns: string[], rows: SqliteRow[]): string {
176
+ if (rows.length === 0) {
177
+ return "(no rows)";
178
+ }
179
+ let nameWidth = MIN_COLUMN_WIDTH;
180
+ for (const column of columns) {
181
+ nameWidth = Math.max(nameWidth, Bun.stringWidth(sanitizeCell(column)));
182
+ }
183
+ nameWidth = Math.min(MAX_COLUMN_WIDTH, nameWidth);
184
+ return rows
185
+ .map((row, index) => {
186
+ const block = [`── Row ${index + 1} ──`];
187
+ for (const column of columns) {
188
+ const name = padCell(column, nameWidth);
189
+ const value = sanitizeCell(stringifySqliteValue(row[column]));
190
+ block.push(truncateToWidth(`${name}: ${value}`, MAX_RENDER_WIDTH));
191
+ }
192
+ return block.join("\n");
193
+ })
194
+ .join("\n\n");
195
+ }
196
+
145
197
  function buildAsciiTable(columns: string[], rows: SqliteRow[]): string {
146
198
  if (columns.length === 0) {
147
199
  return rows.length === 0 ? "(no rows)" : "(rows returned without named columns)";
148
200
  }
201
+ if (!tableFitsAtMinimum(columns.length)) {
202
+ return buildVerticalBlocks(columns, rows);
203
+ }
149
204
 
150
205
  const widths = columns.map(column =>
151
206
  Math.max(MIN_COLUMN_WIDTH, Math.min(MAX_COLUMN_WIDTH, Bun.stringWidth(sanitizeCell(column)))),
@@ -157,7 +212,8 @@ function buildAsciiTable(columns: string[], rows: SqliteRow[]): string {
157
212
  }
158
213
  }
159
214
 
160
- let totalWidth = widths.reduce((sum, width) => sum + width, 0) + columns.length * 3 + 1;
215
+ const overhead = columns.length * COLUMN_SEPARATOR_WIDTH + TABLE_FRAME_WIDTH;
216
+ let totalWidth = widths.reduce((sum, width) => sum + width, 0) + overhead;
161
217
  while (totalWidth > MAX_RENDER_WIDTH) {
162
218
  let widestIndex = -1;
163
219
  let widestWidth = MIN_COLUMN_WIDTH;
@@ -169,7 +225,7 @@ function buildAsciiTable(columns: string[], rows: SqliteRow[]): string {
169
225
  }
170
226
  if (widestIndex === -1) break;
171
227
  widths[widestIndex] = Math.max(MIN_COLUMN_WIDTH, (widths[widestIndex] ?? MIN_COLUMN_WIDTH) - 1);
172
- totalWidth = widths.reduce((sum, width) => sum + width, 0) + columns.length * 3 + 1;
228
+ totalWidth = widths.reduce((sum, width) => sum + width, 0) + overhead;
173
229
  }
174
230
 
175
231
  const header = `| ${columns.map((column, index) => padCell(column, widths[index] ?? MIN_COLUMN_WIDTH)).join(" | ")} |`;
package/src/tools/todo.ts CHANGED
@@ -11,6 +11,7 @@ import todoDescription from "../prompts/tools/todo.md" with { type: "text" };
11
11
  import type { ToolSession } from "../sdk";
12
12
  import type { SessionEntry } from "../session/session-entries";
13
13
  import { framedBlock, renderStatusLine, renderTreeList } from "../tui";
14
+ import { normalizePathLikeInput, resolveToCwd } from "./path-utils";
14
15
  import { formatErrorDetail, PREVIEW_LIMITS } from "./render-utils";
15
16
 
16
17
  // =============================================================================
@@ -428,6 +429,11 @@ const STATUS_TO_MARKER: Record<TodoStatus, string> = {
428
429
  abandoned: "-",
429
430
  };
430
431
 
432
+ export function resolveTodoMarkdownPath(input: string, cwd: string): string {
433
+ const raw = normalizePathLikeInput(input) || "TODO.md";
434
+ return resolveToCwd(raw, cwd);
435
+ }
436
+
431
437
  /** Render todo phases as a Markdown checklist suitable for editing/copying. */
432
438
  export function phasesToMarkdown(phases: TodoPhase[]): string {
433
439
  if (phases.length === 0) return "# Todos\n";
@@ -1068,14 +1068,12 @@ export const writeToolRenderer = {
1068
1068
  const lang = getLanguageFromPath(rawPath) ?? "text";
1069
1069
  const langIcon = uiTheme.fg("muted", uiTheme.getLangIcon(lang));
1070
1070
  const pathDisplay = filePath ? uiTheme.fg("accent", filePath) : uiTheme.fg("toolOutput", "…");
1071
- // Static pending icon, never the animated glyph: the header is the head
1072
- // row of the framed block, and native-scrollback commits are prefix-only
1073
- // — an animating head row would pin the commit boundary at the top and
1074
- // keep a tall expanded preview from scroll-appending mid-stream. The
1075
- // liveness cue rides the trailing "(streaming)" line instead.
1071
+ // No status icon on the head row: it's the head of the framed block, and
1072
+ // native-scrollback commits are prefix-only — an animated glyph would pin
1073
+ // the commit boundary at the top, and the pending hourglass just adds
1074
+ // noise. The liveness cue rides the trailing "(streaming)" line instead.
1076
1075
  const header = renderStatusLine(
1077
1076
  {
1078
- icon: "pending",
1079
1077
  title: "Write",
1080
1078
  description: `${langIcon} ${pathDisplay}`,
1081
1079
  },
@@ -9,3 +9,81 @@ export function canonicalizeMessage(text: string | null | undefined): string {
9
9
  }
10
10
  return "";
11
11
  }
12
+
13
+ export function formatThinkingForDisplay(text: string, proseOnly: boolean): string {
14
+ if (!proseOnly || !text) return text;
15
+
16
+ const lines = text.split("\n");
17
+ const resultLines: string[] = [];
18
+ let inFence = false;
19
+ let fenceChar = "";
20
+ let fenceLen = 0;
21
+
22
+ const FENCE = /^( {0,3})([`~]{3,})/;
23
+ const appendEllipsis = () => {
24
+ let lastLineIdx = resultLines.length - 1;
25
+ while (lastLineIdx >= 0 && resultLines[lastLineIdx]!.trim() === "") {
26
+ lastLineIdx--;
27
+ }
28
+
29
+ if (lastLineIdx >= 0) {
30
+ const lastLine = resultLines[lastLineIdx]!;
31
+ const trimmed = lastLine.trimEnd();
32
+ if (trimmed.endsWith("...")) {
33
+ resultLines[lastLineIdx] = trimmed;
34
+ } else if (trimmed.endsWith(".")) {
35
+ resultLines[lastLineIdx] = `${trimmed.slice(0, -1)}...`;
36
+ } else {
37
+ resultLines[lastLineIdx] = `${trimmed}...`;
38
+ }
39
+ } else {
40
+ resultLines.push("...");
41
+ }
42
+ };
43
+
44
+ for (let i = 0; i < lines.length; i++) {
45
+ const line = lines[i]!;
46
+ const open = FENCE.exec(line);
47
+
48
+ if (inFence) {
49
+ // A closing fence is the same char, at least as long, with nothing else on the line.
50
+ if (
51
+ open &&
52
+ open[2]![0] === fenceChar &&
53
+ open[2]!.length >= fenceLen &&
54
+ line.slice(open[1]!.length + open[2]!.length).trim() === ""
55
+ ) {
56
+ inFence = false;
57
+ fenceChar = "";
58
+ fenceLen = 0;
59
+ }
60
+ // We skip all internal lines of a code fence.
61
+ } else if (open) {
62
+ const marker = open[2]!;
63
+ const ch = marker[0]!;
64
+ // A backtick fence's info string may not contain a backtick.
65
+ if (!(ch === "`" && line.slice(open[1]!.length + marker.length).includes("`"))) {
66
+ inFence = true;
67
+ fenceChar = ch;
68
+ fenceLen = marker.length;
69
+ appendEllipsis();
70
+ } else {
71
+ resultLines.push(line);
72
+ }
73
+ } else {
74
+ resultLines.push(line);
75
+ }
76
+ }
77
+
78
+ const formatted = resultLines.join("\n");
79
+ return formatted;
80
+ }
81
+
82
+ export function hasDisplayableThinking(
83
+ text: string | null | undefined,
84
+ formattedText: string | null | undefined,
85
+ ): boolean {
86
+ if (!text) return false;
87
+ if (!formattedText) return false;
88
+ return formattedText.length > 0 && canonicalizeMessage(text).length > 0;
89
+ }