@oh-my-pi/pi-coding-agent 17.0.2 → 17.0.4
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.
- package/CHANGELOG.md +80 -20
- package/dist/cli.js +4087 -4108
- package/dist/types/config/settings-schema.d.ts +7 -3
- package/dist/types/eval/agent-bridge.d.ts +7 -19
- package/dist/types/extensibility/legacy-pi-coding-agent-shim.d.ts +15 -1
- package/dist/types/lsp/client.d.ts +2 -0
- package/dist/types/lsp/types.d.ts +3 -0
- package/dist/types/mcp/transports/stdio.d.ts +29 -0
- package/dist/types/mnemopi/state.d.ts +32 -9
- package/dist/types/modes/components/agent-hub.d.ts +15 -0
- package/dist/types/registry/persisted-agents.d.ts +3 -0
- package/dist/types/sdk.d.ts +14 -3
- package/dist/types/session/session-entries.d.ts +6 -1
- package/dist/types/session/session-manager.d.ts +5 -0
- package/dist/types/task/executor.d.ts +26 -1
- package/dist/types/task/index.d.ts +1 -1
- package/dist/types/task/parallel.d.ts +14 -0
- package/dist/types/task/structured-subagent.d.ts +111 -0
- package/dist/types/task/types.d.ts +51 -0
- package/dist/types/tools/fetch.d.ts +4 -5
- package/dist/types/tools/hub/messaging.d.ts +5 -1
- package/dist/types/tools/index.d.ts +16 -1
- package/dist/types/tools/todo.d.ts +31 -0
- package/dist/types/tui/tree-list.d.ts +7 -0
- package/dist/types/utils/markit.d.ts +11 -0
- package/package.json +12 -12
- package/src/cli/file-processor.ts +1 -2
- package/src/config/settings-schema.ts +4 -3
- package/src/eval/__tests__/agent-bridge.test.ts +133 -47
- package/src/eval/__tests__/prelude-agent.test.ts +29 -0
- package/src/eval/agent-bridge.ts +104 -477
- package/src/eval/jl/prelude.jl +7 -6
- package/src/eval/js/shared/prelude.txt +5 -4
- package/src/eval/py/prelude.py +11 -39
- package/src/eval/rb/prelude.rb +5 -6
- package/src/extensibility/legacy-pi-coding-agent-shim.ts +53 -7
- package/src/lsp/client.ts +57 -0
- package/src/lsp/index.ts +62 -6
- package/src/lsp/types.ts +3 -0
- package/src/mcp/oauth-flow.ts +20 -0
- package/src/mcp/transports/stdio.test.ts +269 -1
- package/src/mcp/transports/stdio.ts +152 -1
- package/src/mnemopi/backend.ts +1 -1
- package/src/mnemopi/embed-worker.ts +8 -7
- package/src/mnemopi/state.ts +45 -18
- package/src/modes/components/agent-hub.ts +1 -72
- package/src/modes/components/bash-execution.ts +7 -3
- package/src/modes/components/eval-execution.ts +3 -1
- package/src/modes/components/transcript-container.ts +13 -7
- package/src/modes/interactive-mode.ts +30 -8
- package/src/prompts/system/system-prompt.md +1 -1
- package/src/prompts/tools/eval.md +5 -2
- package/src/prompts/tools/read.md +1 -1
- package/src/prompts/tools/task.md +12 -8
- package/src/prompts/tools/write.md +1 -1
- package/src/registry/persisted-agents.ts +74 -0
- package/src/sdk.ts +129 -87
- package/src/session/agent-session.ts +75 -21
- package/src/session/session-entries.ts +6 -1
- package/src/session/session-loader.ts +31 -3
- package/src/session/session-manager.ts +9 -0
- package/src/session/streaming-output.ts +41 -1
- package/src/stt/recorder.ts +68 -55
- package/src/system-prompt.ts +7 -2
- package/src/task/executor.ts +99 -21
- package/src/task/index.ts +258 -429
- package/src/task/parallel.ts +43 -0
- package/src/task/persisted-revive.ts +19 -7
- package/src/task/structured-subagent.ts +642 -0
- package/src/task/types.ts +61 -0
- package/src/tools/__tests__/eval-description.test.ts +1 -1
- package/src/tools/fetch.ts +28 -105
- package/src/tools/hub/messaging.ts +16 -3
- package/src/tools/index.ts +47 -14
- package/src/tools/path-utils.ts +1 -0
- package/src/tools/read.ts +14 -26
- package/src/tools/todo.ts +126 -13
- package/src/tui/tree-list.ts +39 -0
- package/src/utils/markit.ts +12 -0
- package/src/utils/zip.ts +16 -1
package/src/tools/todo.ts
CHANGED
|
@@ -12,7 +12,7 @@ import type { ToolSession } from "../sdk";
|
|
|
12
12
|
import type { SessionEntry } from "../session/session-entries";
|
|
13
13
|
import { framedBlock, renderStatusLine, renderTreeList } from "../tui";
|
|
14
14
|
import { normalizePathLikeInput, resolveToCwd } from "./path-utils";
|
|
15
|
-
import { formatErrorDetail, PREVIEW_LIMITS } from "./render-utils";
|
|
15
|
+
import { formatErrorDetail, formatMoreItems, PREVIEW_LIMITS, pluralize } from "./render-utils";
|
|
16
16
|
|
|
17
17
|
// =============================================================================
|
|
18
18
|
// Types
|
|
@@ -214,6 +214,80 @@ export function todoMatchesAnyDescription(content: string, descriptions: readonl
|
|
|
214
214
|
return false;
|
|
215
215
|
}
|
|
216
216
|
|
|
217
|
+
/**
|
|
218
|
+
* A todo the collapsed viewport treats as current work: the literal
|
|
219
|
+
* `in_progress` task or a pending task a live subagent is executing. Both
|
|
220
|
+
* collapsed views (transient tool result + sticky HUD) run this same policy so
|
|
221
|
+
* they can never disagree about what the agent is doing (#5873).
|
|
222
|
+
*/
|
|
223
|
+
function isActiveTodo<T extends { status: TodoStatus }>(task: T, isMatched: (task: T) => boolean): boolean {
|
|
224
|
+
return task.status === "in_progress" || (task.status === "pending" && isMatched(task));
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
/** Result of {@link selectCollapsedTodos}: the rows to render plus an optional
|
|
228
|
+
* summary line (empty string ⇒ no summary row). */
|
|
229
|
+
export interface CollapsedTodoSelection<T> {
|
|
230
|
+
items: T[];
|
|
231
|
+
summary: string;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
/**
|
|
235
|
+
* Walking-viewport selection for a phase's collapsed todo preview (#5873).
|
|
236
|
+
*
|
|
237
|
+
* Policy, applied to `tasks` in todo order:
|
|
238
|
+
* 1. While the phase has open work, completed/abandoned tasks are omitted. A
|
|
239
|
+
* phase with no open tasks left falls back to its closed tasks so the sticky
|
|
240
|
+
* HUD's closed-todo persistence still has something to render.
|
|
241
|
+
* 2. Every active task (in-progress, or pending matched to a live subagent) is
|
|
242
|
+
* placed at the head in stable todo order — never dropped for lying outside
|
|
243
|
+
* an ordinary window.
|
|
244
|
+
* 3. Remaining rows up to `cap` are filled with the pending tasks that follow
|
|
245
|
+
* the first active one, in todo order (falling back to leading pending tasks
|
|
246
|
+
* when no active task exists), so a freshly-promoted task leads the preview.
|
|
247
|
+
* 4. When active tasks alone exceed `cap`, only the first `cap` active tasks are
|
|
248
|
+
* shown and the summary counts the hidden *active* todos, never replacing
|
|
249
|
+
* them with unrelated pending rows.
|
|
250
|
+
*
|
|
251
|
+
* The summary otherwise counts the remaining tasks in the display base. Returns
|
|
252
|
+
* the whole base with an empty summary when it already fits.
|
|
253
|
+
*/
|
|
254
|
+
export function selectCollapsedTodos<T extends { status: TodoStatus }>(
|
|
255
|
+
tasks: T[],
|
|
256
|
+
isMatched: (task: T) => boolean,
|
|
257
|
+
cap: number,
|
|
258
|
+
): CollapsedTodoSelection<T> {
|
|
259
|
+
const open = tasks.filter(task => task.status === "pending" || task.status === "in_progress");
|
|
260
|
+
// No open work: fall back to the closed tasks so a settled phase still
|
|
261
|
+
// renders (HUD closed-todo persistence). Closed tasks are never active.
|
|
262
|
+
const base = open.length > 0 ? open : tasks;
|
|
263
|
+
if (base.length <= cap) return { items: base, summary: "" };
|
|
264
|
+
|
|
265
|
+
const active = base.filter(task => isActiveTodo(task, isMatched));
|
|
266
|
+
// Only when active work strictly exceeds the cap do we drop pending rows and
|
|
267
|
+
// count hidden *actives*. At exactly `cap` actives, fall through so the normal
|
|
268
|
+
// branch still surfaces any following pending work in the summary.
|
|
269
|
+
if (active.length > cap) {
|
|
270
|
+
const hiddenActive = active.length - cap;
|
|
271
|
+
return {
|
|
272
|
+
items: active.slice(0, cap),
|
|
273
|
+
summary: `… ${hiddenActive} more active ${pluralize("todo", hiddenActive)}`,
|
|
274
|
+
};
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
// Fill trailing rows with tasks following the first active one, so the
|
|
278
|
+
// promoted/current task leads and its successors follow in todo order.
|
|
279
|
+
const firstActiveIdx = active.length > 0 ? base.indexOf(active[0]) : 0;
|
|
280
|
+
const fill: T[] = [];
|
|
281
|
+
for (let i = firstActiveIdx; i < base.length && active.length + fill.length < cap; i++) {
|
|
282
|
+
const task = base[i];
|
|
283
|
+
if (isActiveTodo(task, isMatched)) continue;
|
|
284
|
+
fill.push(task);
|
|
285
|
+
}
|
|
286
|
+
const items = [...active, ...fill];
|
|
287
|
+
const hidden = base.length - items.length;
|
|
288
|
+
return { items, summary: hidden > 0 ? formatMoreItems(hidden, "todo") : "" };
|
|
289
|
+
}
|
|
290
|
+
|
|
217
291
|
function resolveTaskOrError(
|
|
218
292
|
phases: TodoPhase[],
|
|
219
293
|
content: string | undefined,
|
|
@@ -755,6 +829,7 @@ function formatTodoLine(
|
|
|
755
829
|
prefix: string,
|
|
756
830
|
completionKeys: Set<string>,
|
|
757
831
|
frame: number | undefined,
|
|
832
|
+
matched = false,
|
|
758
833
|
): string {
|
|
759
834
|
const checkbox = uiTheme.checkbox;
|
|
760
835
|
switch (item.status) {
|
|
@@ -771,7 +846,9 @@ function formatTodoLine(
|
|
|
771
846
|
case "abandoned":
|
|
772
847
|
return uiTheme.fg("error", `${prefix}${checkbox.unchecked} ${strikethroughText(item.content)}`);
|
|
773
848
|
default:
|
|
774
|
-
|
|
849
|
+
// A pending todo lit by a live subagent match renders accent, matching
|
|
850
|
+
// the sticky HUD's convention (#5873).
|
|
851
|
+
return uiTheme.fg(matched ? "accent" : "dim", `${prefix}${checkbox.unchecked} ${item.content}`);
|
|
775
852
|
}
|
|
776
853
|
}
|
|
777
854
|
|
|
@@ -822,6 +899,21 @@ function formatPhaseSummary(phase: TodoPhase, oneBasedIndex: number, uiTheme: Th
|
|
|
822
899
|
return `${name}${uiTheme.fg("dim", ` ${done}/${total}`)}`;
|
|
823
900
|
}
|
|
824
901
|
|
|
902
|
+
/**
|
|
903
|
+
* Live subagent descriptions the transient tool result uses to detect
|
|
904
|
+
* pending todos being executed by an in-flight subagent, so its collapsed
|
|
905
|
+
* viewport surfaces the same active work the sticky HUD does (#5873). Wired
|
|
906
|
+
* once by interactive mode from its observer registry; returns `[]` outside an
|
|
907
|
+
* interactive session (tests, SDK, transcript rebuilds), where only literal
|
|
908
|
+
* `in_progress` counts as active.
|
|
909
|
+
*/
|
|
910
|
+
let activeTodoDescriptionsProvider: () => readonly string[] = () => [];
|
|
911
|
+
|
|
912
|
+
/** Wire the live-subagent description source for {@link todoToolRenderer}. */
|
|
913
|
+
export function setActiveTodoDescriptionsProvider(provider: () => readonly string[]): void {
|
|
914
|
+
activeTodoDescriptionsProvider = provider;
|
|
915
|
+
}
|
|
916
|
+
|
|
825
917
|
export const todoToolRenderer = {
|
|
826
918
|
renderCall(args: TodoRenderArgs, options: RenderResultOptions, uiTheme: Theme): Component {
|
|
827
919
|
// `args` is the raw partially-parsed JSON from the streaming tool-call
|
|
@@ -903,6 +995,12 @@ export const todoToolRenderer = {
|
|
|
903
995
|
// a single task flip doesn't redraw every phase's full task list. The
|
|
904
996
|
// manual expand toggle (and the no-signal fallback) still shows all.
|
|
905
997
|
const touched = expanded || !multiPhase ? null : computeTouchedPhases(args, phases, completedTasks);
|
|
998
|
+
// A pending todo counts as active work when an in-flight subagent is
|
|
999
|
+
// executing it — the transient result surfaces the same active set the
|
|
1000
|
+
// sticky HUD does (#5873). Empty outside an interactive session.
|
|
1001
|
+
const activeDescs = expanded ? [] : activeTodoDescriptionsProvider();
|
|
1002
|
+
const isMatched = (task: TodoItem): boolean =>
|
|
1003
|
+
activeDescs.length > 0 && todoMatchesAnyDescription(task.content, activeDescs);
|
|
906
1004
|
const bodyLines: string[] = [];
|
|
907
1005
|
for (let p = 0; p < phases.length; p++) {
|
|
908
1006
|
const phase = phases[p];
|
|
@@ -914,17 +1012,32 @@ export const todoToolRenderer = {
|
|
|
914
1012
|
bodyLines.push(uiTheme.fg("accent", chalk.bold(formatPhaseDisplayName(phase.name, p + 1))));
|
|
915
1013
|
}
|
|
916
1014
|
const completionKeys = completionKeysByPhase.get(phase.name) ?? EMPTY_COMPLETION_KEYS;
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
1015
|
+
// Collapsed: walking viewport — completed/abandoned omitted, active
|
|
1016
|
+
// work (in-progress / subagent-matched) pulled to the head, then
|
|
1017
|
+
// following pending tasks (#5873). Expanded: every task in order.
|
|
1018
|
+
const treeLines = expanded
|
|
1019
|
+
? renderTreeList(
|
|
1020
|
+
{
|
|
1021
|
+
items: phase.tasks,
|
|
1022
|
+
expanded,
|
|
1023
|
+
itemType: "todo",
|
|
1024
|
+
renderItem: todo => formatTodoLine(todo, uiTheme, "", completionKeys, spinnerFrame),
|
|
1025
|
+
},
|
|
1026
|
+
uiTheme,
|
|
1027
|
+
)
|
|
1028
|
+
: (() => {
|
|
1029
|
+
const selection = selectCollapsedTodos(phase.tasks, isMatched, PREVIEW_LIMITS.COLLAPSED_ITEMS);
|
|
1030
|
+
return renderTreeList(
|
|
1031
|
+
{
|
|
1032
|
+
items: selection.items,
|
|
1033
|
+
itemType: "todo",
|
|
1034
|
+
trailingSummary: selection.summary,
|
|
1035
|
+
renderItem: todo =>
|
|
1036
|
+
formatTodoLine(todo, uiTheme, "", completionKeys, spinnerFrame, isMatched(todo)),
|
|
1037
|
+
},
|
|
1038
|
+
uiTheme,
|
|
1039
|
+
);
|
|
1040
|
+
})();
|
|
928
1041
|
for (const line of treeLines) {
|
|
929
1042
|
bodyLines.push(`${indent}${line}`);
|
|
930
1043
|
}
|
package/src/tui/tree-list.ts
CHANGED
|
@@ -18,6 +18,13 @@ export interface TreeListOptions<T> {
|
|
|
18
18
|
maxCollapsedLines?: number;
|
|
19
19
|
itemType?: string;
|
|
20
20
|
truncateFrom?: "start" | "end";
|
|
21
|
+
/** Caller-supplied trailing summary line. When set (and not expanded),
|
|
22
|
+
* `renderTreeList` renders exactly the provided `items` (the caller has
|
|
23
|
+
* already applied its own selection/cap) and appends this text as the
|
|
24
|
+
* final `└` row, with the last item using `├`. Empty string renders the
|
|
25
|
+
* items with no summary. Bypasses the built-in truncation/`maxCollapsed`
|
|
26
|
+
* path. */
|
|
27
|
+
trailingSummary?: string;
|
|
21
28
|
/** Called once per item with `isLast: false` during budget calculation;
|
|
22
29
|
* line count MUST NOT vary based on `isLast`. */
|
|
23
30
|
renderItem: (item: T, context: TreeContext) => string | string[];
|
|
@@ -36,6 +43,38 @@ export function renderTreeList<T>(options: TreeListOptions<T>, theme: Theme): st
|
|
|
36
43
|
const maxItems = expanded ? items.length : Math.min(items.length, maxCollapsed);
|
|
37
44
|
const linesBudget = !expanded && maxCollapsedLines !== undefined ? maxCollapsedLines : Infinity;
|
|
38
45
|
|
|
46
|
+
// Caller-driven collapse: render exactly the provided items (the caller
|
|
47
|
+
// already picked/capped them) plus an optional trailing summary row. The
|
|
48
|
+
// walking-viewport todo policy uses this so item selection lives in the
|
|
49
|
+
// todo domain, not here.
|
|
50
|
+
if (!expanded && options.trailingSummary !== undefined) {
|
|
51
|
+
const summary = options.trailingSummary;
|
|
52
|
+
const lines: string[] = [];
|
|
53
|
+
for (let i = 0; i < items.length; i++) {
|
|
54
|
+
const rendered = renderItem(items[i], {
|
|
55
|
+
index: i,
|
|
56
|
+
isLast: false,
|
|
57
|
+
depth: 0,
|
|
58
|
+
theme,
|
|
59
|
+
prefix: "",
|
|
60
|
+
continuePrefix: "",
|
|
61
|
+
});
|
|
62
|
+
const itemLines = Array.isArray(rendered) ? rendered : rendered ? [rendered] : [];
|
|
63
|
+
if (itemLines.length === 0) continue;
|
|
64
|
+
const isLast = summary === "" && i === items.length - 1;
|
|
65
|
+
const prefix = `${theme.fg("dim", getTreeBranch(isLast, theme))} `;
|
|
66
|
+
const continuePrefix = `${theme.fg("dim", getTreeContinuePrefix(isLast, theme))}`;
|
|
67
|
+
lines.push(`${prefix}${replaceTabs(itemLines[0]!)}`);
|
|
68
|
+
for (let j = 1; j < itemLines.length; j++) {
|
|
69
|
+
lines.push(`${continuePrefix}${replaceTabs(itemLines[j]!)}`);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
if (summary !== "") {
|
|
73
|
+
lines.push(`${theme.fg("dim", theme.tree.last)} ${theme.fg("muted", summary)}`);
|
|
74
|
+
}
|
|
75
|
+
return lines;
|
|
76
|
+
}
|
|
77
|
+
|
|
39
78
|
const candidateIndices: number[] = [];
|
|
40
79
|
if (truncateFrom === "start") {
|
|
41
80
|
const startCandidateIdx = Math.max(0, items.length - maxItems);
|
package/src/utils/markit.ts
CHANGED
|
@@ -10,6 +10,18 @@ import {
|
|
|
10
10
|
} from "./markit-cache";
|
|
11
11
|
import { loadEmbeddedMupdfWasm } from "./mupdf-wasm-embed";
|
|
12
12
|
|
|
13
|
+
/**
|
|
14
|
+
* File extensions markit can actually convert to markdown — one per registered
|
|
15
|
+
* converter in `src/markit/registry.ts` (pdf, docx, pptx, xlsx, epub). This is
|
|
16
|
+
* the single source of truth shared by the read, fetch, and CLI file tools so
|
|
17
|
+
* the advertised set never drifts from the converters that back it. Legacy
|
|
18
|
+
* binary formats (`.doc`, `.ppt`, `.xls`, `.rtf`) are intentionally absent:
|
|
19
|
+
* markit has no converter for them, so routing them here only produced an
|
|
20
|
+
* `Unsupported format` error instead of letting them fall through to the
|
|
21
|
+
* binary-file handling.
|
|
22
|
+
*/
|
|
23
|
+
export const CONVERTIBLE_EXTENSIONS: ReadonlySet<string> = new Set([".pdf", ".docx", ".pptx", ".xlsx", ".epub"]);
|
|
24
|
+
|
|
13
25
|
export interface MarkitConversionResult {
|
|
14
26
|
content: string;
|
|
15
27
|
ok: boolean;
|
package/src/utils/zip.ts
CHANGED
|
@@ -233,12 +233,27 @@ function ensureParentDirectories(map: Map<string, ArchiveIndexEntry>): void {
|
|
|
233
233
|
}
|
|
234
234
|
}
|
|
235
235
|
|
|
236
|
+
/**
|
|
237
|
+
* Extensions that are ZIP containers under a different name — JVM (`.jar`,
|
|
238
|
+
* `.war`, `.ear`) and Android (`.apk`) packages are all ZIP archives. Treated
|
|
239
|
+
* as `zip` for member read/list and whole-archive rewrite.
|
|
240
|
+
*/
|
|
241
|
+
const ZIP_ALIAS_EXTENSIONS = ["jar", "war", "ear", "apk"] as const;
|
|
242
|
+
|
|
243
|
+
/**
|
|
244
|
+
* Regex alternation of every recognized archive extension, longest first so
|
|
245
|
+
* `.tar.gz` wins over `.tar`. Shared with `parseArchivePathCandidates` as its
|
|
246
|
+
* split pattern so extension recognition and path splitting never drift.
|
|
247
|
+
*/
|
|
248
|
+
const ARCHIVE_EXTENSION_ALTERNATION = ["tar\\.gz", "tgz", "zip", "tar", ...ZIP_ALIAS_EXTENSIONS].join("|");
|
|
249
|
+
|
|
236
250
|
/** Infer an archive format from a filesystem path's extension. */
|
|
237
251
|
export function archiveFormatFromPath(filePath: string): ArchiveFormat | undefined {
|
|
238
252
|
const normalized = filePath.toLowerCase();
|
|
239
253
|
if (normalized.endsWith(".tar.gz") || normalized.endsWith(".tgz")) return "tar.gz";
|
|
240
254
|
if (normalized.endsWith(".tar")) return "tar";
|
|
241
255
|
if (normalized.endsWith(".zip")) return "zip";
|
|
256
|
+
if (ZIP_ALIAS_EXTENSIONS.some(ext => normalized.endsWith(`.${ext}`))) return "zip";
|
|
242
257
|
return undefined;
|
|
243
258
|
}
|
|
244
259
|
|
|
@@ -635,7 +650,7 @@ async function readZipEntries(source: ByteSource): Promise<ArchiveIndexEntry[]>
|
|
|
635
650
|
*/
|
|
636
651
|
export function parseArchivePathCandidates(filePath: string): ArchivePathCandidate[] {
|
|
637
652
|
const normalized = filePath.replace(/\\/g, "/");
|
|
638
|
-
const pattern =
|
|
653
|
+
const pattern = new RegExp(`\\.(?:${ARCHIVE_EXTENSION_ALTERNATION})(?=(?::|$))`, "gi");
|
|
639
654
|
const seen = new Set<string>();
|
|
640
655
|
const candidates: ArchivePathCandidate[] = [];
|
|
641
656
|
|