@oh-my-pi/pi-coding-agent 14.5.13 → 14.6.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.
- package/CHANGELOG.md +52 -0
- package/package.json +7 -7
- package/src/autoresearch/command-resume.md +5 -8
- package/src/autoresearch/git.ts +41 -51
- package/src/autoresearch/helpers.ts +43 -359
- package/src/autoresearch/index.ts +281 -273
- package/src/autoresearch/prompt-setup.md +43 -0
- package/src/autoresearch/prompt.md +52 -193
- package/src/autoresearch/resume-message.md +2 -8
- package/src/autoresearch/state.ts +59 -166
- package/src/autoresearch/storage.ts +687 -0
- package/src/autoresearch/tools/init-experiment.ts +201 -290
- package/src/autoresearch/tools/log-experiment.ts +304 -517
- package/src/autoresearch/tools/run-experiment.ts +117 -296
- package/src/autoresearch/tools/update-notes.ts +116 -0
- package/src/autoresearch/types.ts +16 -66
- package/src/commit/pipeline.ts +4 -3
- package/src/config/settings-schema.ts +1 -1
- package/src/config/settings.ts +20 -1
- package/src/config.ts +9 -6
- package/src/cursor.ts +1 -1
- package/src/edit/index.ts +9 -31
- package/src/edit/line-hash.ts +70 -43
- package/src/edit/modes/hashline.lark +26 -0
- package/src/edit/modes/hashline.ts +898 -1099
- package/src/edit/modes/patch.ts +0 -7
- package/src/edit/modes/replace.ts +0 -4
- package/src/edit/renderer.ts +22 -20
- package/src/edit/streaming.ts +8 -28
- package/src/eval/eval.lark +24 -30
- package/src/eval/js/context-manager.ts +5 -162
- package/src/eval/js/prelude.txt +0 -12
- package/src/eval/parse.ts +129 -129
- package/src/eval/py/kernel.ts +4 -4
- package/src/eval/py/prelude.py +1 -219
- package/src/export/html/template.generated.ts +1 -1
- package/src/export/html/template.js +2 -2
- package/src/internal-urls/docs-index.generated.ts +1 -1
- package/src/main.ts +10 -0
- package/src/mcp/manager.ts +22 -0
- package/src/modes/components/session-observer-overlay.ts +5 -2
- package/src/modes/components/status-line/segments.ts +1 -1
- package/src/modes/components/status-line.ts +3 -5
- package/src/modes/components/tree-selector.ts +4 -5
- package/src/modes/components/welcome.ts +11 -1
- package/src/modes/controllers/command-controller.ts +2 -6
- package/src/modes/controllers/event-controller.ts +1 -2
- package/src/modes/controllers/extension-ui-controller.ts +3 -15
- package/src/modes/controllers/input-controller.ts +0 -1
- package/src/modes/controllers/selector-controller.ts +1 -1
- package/src/modes/interactive-mode.ts +5 -7
- package/src/modes/rpc/rpc-client.ts +9 -0
- package/src/modes/rpc/rpc-mode.ts +6 -0
- package/src/modes/rpc/rpc-types.ts +9 -0
- package/src/prompts/system/system-prompt.md +14 -38
- package/src/prompts/tools/ast-edit.md +8 -8
- package/src/prompts/tools/ast-grep.md +10 -10
- package/src/prompts/tools/eval.md +13 -31
- package/src/prompts/tools/find.md +2 -1
- package/src/prompts/tools/hashline.md +66 -57
- package/src/prompts/tools/search.md +2 -2
- package/src/sdk.ts +19 -4
- package/src/session/agent-session.ts +110 -4
- package/src/session/session-manager.ts +17 -13
- package/src/task/agents.ts +4 -5
- package/src/tools/archive-reader.ts +9 -3
- package/src/tools/ast-edit.ts +141 -44
- package/src/tools/ast-grep.ts +112 -36
- package/src/tools/browser/readable.ts +11 -6
- package/src/tools/browser/tab-supervisor.ts +2 -2
- package/src/tools/browser.ts +5 -3
- package/src/tools/eval.ts +2 -53
- package/src/tools/find.ts +16 -15
- package/src/tools/image-gen.ts +2 -2
- package/src/tools/path-utils.ts +36 -196
- package/src/tools/search.ts +56 -35
- package/src/tools/write.ts +8 -1
- package/src/utils/edit-mode.ts +2 -11
- package/src/utils/file-display-mode.ts +1 -1
- package/src/utils/git.ts +17 -0
- package/src/utils/session-color.ts +0 -12
- package/src/utils/title-generator.ts +22 -38
- package/src/web/scrapers/crossref.ts +3 -3
- package/src/web/scrapers/devto.ts +1 -1
- package/src/web/scrapers/discourse.ts +5 -5
- package/src/web/scrapers/firefox-addons.ts +1 -1
- package/src/web/scrapers/flathub.ts +2 -2
- package/src/web/scrapers/gitlab.ts +1 -1
- package/src/web/scrapers/go-pkg.ts +2 -2
- package/src/web/scrapers/jetbrains-marketplace.ts +1 -1
- package/src/web/scrapers/mastodon.ts +9 -9
- package/src/web/scrapers/mdn.ts +11 -7
- package/src/web/scrapers/pub-dev.ts +1 -1
- package/src/web/scrapers/rawg.ts +3 -3
- package/src/web/scrapers/readthedocs.ts +1 -1
- package/src/web/scrapers/spdx.ts +1 -1
- package/src/web/scrapers/stackoverflow.ts +2 -2
- package/src/web/scrapers/types.ts +53 -39
- package/src/web/scrapers/w3c.ts +1 -1
- package/src/web/search/providers/gemini.ts +2 -2
- package/src/autoresearch/apply-contract-to-state.ts +0 -24
- package/src/autoresearch/contract.ts +0 -288
- package/src/edit/modes/atom.lark +0 -29
- package/src/edit/modes/atom.ts +0 -1773
- package/src/prompts/tools/atom.md +0 -150
package/src/sdk.ts
CHANGED
|
@@ -300,7 +300,6 @@ function getDefaultAgentDir(): string {
|
|
|
300
300
|
*/
|
|
301
301
|
export async function discoverAuthStorage(agentDir: string = getDefaultAgentDir()): Promise<AuthStorage> {
|
|
302
302
|
const dbPath = getAgentDbPath(agentDir);
|
|
303
|
-
logger.debug("discoverAuthStorage", { agentDir, dbPath });
|
|
304
303
|
|
|
305
304
|
const storage = await AuthStorage.create(dbPath, { configValueResolver: resolveConfigValue });
|
|
306
305
|
await storage.reload();
|
|
@@ -429,6 +428,9 @@ function isCustomTool(tool: CustomTool | ToolDefinition): tool is CustomTool {
|
|
|
429
428
|
|
|
430
429
|
const TOOL_DEFINITION_MARKER = Symbol("__isToolDefinition");
|
|
431
430
|
|
|
431
|
+
/** Matches the truncation applied to per-server instructions inside `rebuildSystemPrompt`. */
|
|
432
|
+
const MAX_MCP_INSTRUCTIONS_LENGTH = 4000;
|
|
433
|
+
|
|
432
434
|
let sshCleanupRegistered = false;
|
|
433
435
|
|
|
434
436
|
async function cleanupSshResources(): Promise<void> {
|
|
@@ -1338,7 +1340,6 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
|
|
|
1338
1340
|
const serverInstructions = mcpManager?.getServerInstructions();
|
|
1339
1341
|
let appendPrompt: string | undefined = memoryInstructions ?? undefined;
|
|
1340
1342
|
if (serverInstructions && serverInstructions.size > 0) {
|
|
1341
|
-
const MAX_INSTRUCTIONS_LENGTH = 4000;
|
|
1342
1343
|
const parts: string[] = [];
|
|
1343
1344
|
if (appendPrompt) parts.push(appendPrompt);
|
|
1344
1345
|
parts.push(
|
|
@@ -1346,8 +1347,8 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
|
|
|
1346
1347
|
);
|
|
1347
1348
|
for (const [srvName, srvInstructions] of serverInstructions) {
|
|
1348
1349
|
const truncated =
|
|
1349
|
-
srvInstructions.length >
|
|
1350
|
-
? `${srvInstructions.slice(0,
|
|
1350
|
+
srvInstructions.length > MAX_MCP_INSTRUCTIONS_LENGTH
|
|
1351
|
+
? `${srvInstructions.slice(0, MAX_MCP_INSTRUCTIONS_LENGTH)}\n[truncated]`
|
|
1351
1352
|
: srvInstructions;
|
|
1352
1353
|
parts.push(`### ${srvName}\n${truncated}`);
|
|
1353
1354
|
}
|
|
@@ -1627,6 +1628,20 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
|
|
|
1627
1628
|
onResponse,
|
|
1628
1629
|
convertToLlm: convertToLlmFinal,
|
|
1629
1630
|
rebuildSystemPrompt,
|
|
1631
|
+
getMcpServerInstructions: mcpManager
|
|
1632
|
+
? () => {
|
|
1633
|
+
const raw = mcpManager.getServerInstructions();
|
|
1634
|
+
if (!raw || raw.size === 0) return raw;
|
|
1635
|
+
const out = new Map<string, string>();
|
|
1636
|
+
for (const [name, text] of raw) {
|
|
1637
|
+
out.set(
|
|
1638
|
+
name,
|
|
1639
|
+
text.length > MAX_MCP_INSTRUCTIONS_LENGTH ? text.slice(0, MAX_MCP_INSTRUCTIONS_LENGTH) : text,
|
|
1640
|
+
);
|
|
1641
|
+
}
|
|
1642
|
+
return out;
|
|
1643
|
+
}
|
|
1644
|
+
: undefined,
|
|
1630
1645
|
mcpDiscoveryEnabled,
|
|
1631
1646
|
initialSelectedMCPToolNames,
|
|
1632
1647
|
defaultSelectedMCPToolNames,
|
|
@@ -244,6 +244,13 @@ export interface AgentSessionConfig {
|
|
|
244
244
|
convertToLlm?: (messages: AgentMessage[]) => Message[] | Promise<Message[]>;
|
|
245
245
|
/** System prompt builder that can consider tool availability */
|
|
246
246
|
rebuildSystemPrompt?: (toolNames: string[], tools: Map<string, AgentTool>) => Promise<string>;
|
|
247
|
+
/**
|
|
248
|
+
* Optional accessor for live MCP server instructions. Read by the session's
|
|
249
|
+
* `rebuildSystemPrompt`-skip optimization to detect server-side instruction
|
|
250
|
+
* changes (e.g. an MCP server upgrade) that would otherwise pass the tool-set
|
|
251
|
+
* signature comparison and silently keep a stale prompt cached.
|
|
252
|
+
*/
|
|
253
|
+
getMcpServerInstructions?: () => Map<string, string> | undefined;
|
|
247
254
|
/** Enable hidden-by-default MCP tool discovery for this session. */
|
|
248
255
|
mcpDiscoveryEnabled?: boolean;
|
|
249
256
|
/** MCP tool names to activate for the current session when discovery mode is enabled. */
|
|
@@ -511,7 +518,15 @@ export class AgentSession {
|
|
|
511
518
|
#onResponse: SimpleStreamOptions["onResponse"] | undefined;
|
|
512
519
|
#convertToLlm: (messages: AgentMessage[]) => Message[] | Promise<Message[]>;
|
|
513
520
|
#rebuildSystemPrompt: ((toolNames: string[], tools: Map<string, AgentTool>) => Promise<string>) | undefined;
|
|
521
|
+
#getMcpServerInstructions: (() => Map<string, string> | undefined) | undefined;
|
|
514
522
|
#baseSystemPrompt: string;
|
|
523
|
+
/**
|
|
524
|
+
* Signature of the (toolNames, tool descriptions) tuple passed to the most
|
|
525
|
+
* recent successful `rebuildSystemPrompt` call. Used to skip redundant rebuilds
|
|
526
|
+
* when MCP servers reconnect without changing their tool definitions, which is
|
|
527
|
+
* the dominant cause of prompt-cache invalidation in long sessions.
|
|
528
|
+
*/
|
|
529
|
+
#lastAppliedToolSignature: string | undefined;
|
|
515
530
|
#mcpDiscoveryEnabled = false;
|
|
516
531
|
#discoverableMCPTools = new Map<string, DiscoverableMCPTool>();
|
|
517
532
|
#discoverableMCPSearchIndex: DiscoverableMCPSearchIndex | null = null;
|
|
@@ -595,6 +610,7 @@ export class AgentSession {
|
|
|
595
610
|
this.#onResponse = config.onResponse;
|
|
596
611
|
this.#convertToLlm = config.convertToLlm ?? convertToLlm;
|
|
597
612
|
this.#rebuildSystemPrompt = config.rebuildSystemPrompt;
|
|
613
|
+
this.#getMcpServerInstructions = config.getMcpServerInstructions;
|
|
598
614
|
this.#baseSystemPrompt = this.agent.state.systemPrompt;
|
|
599
615
|
this.#mcpDiscoveryEnabled = config.mcpDiscoveryEnabled ?? false;
|
|
600
616
|
this.#setDiscoverableMCPTools(this.#collectDiscoverableMCPToolsFromRegistry());
|
|
@@ -2211,10 +2227,18 @@ export class AgentSession {
|
|
|
2211
2227
|
}
|
|
2212
2228
|
this.agent.setTools(tools);
|
|
2213
2229
|
|
|
2214
|
-
// Rebuild base system prompt with new tool set
|
|
2230
|
+
// Rebuild base system prompt with new tool set, but only when the tool set
|
|
2231
|
+
// actually changed. MCP servers can reconnect at arbitrary times and call
|
|
2232
|
+
// `refreshMCPTools` -> `#applyActiveToolsByName` even though the resulting
|
|
2233
|
+
// tool list is byte-identical. Skipping the rebuild keeps the system prompt
|
|
2234
|
+
// stable, which is required for Anthropic prompt caching to keep hitting.
|
|
2215
2235
|
if (this.#rebuildSystemPrompt) {
|
|
2216
|
-
|
|
2217
|
-
|
|
2236
|
+
const signature = this.#computeAppliedToolSignature(validToolNames, tools);
|
|
2237
|
+
if (signature !== this.#lastAppliedToolSignature) {
|
|
2238
|
+
this.#baseSystemPrompt = await this.#rebuildSystemPrompt(validToolNames, this.#toolRegistry);
|
|
2239
|
+
this.agent.setSystemPrompt(this.#baseSystemPrompt);
|
|
2240
|
+
this.#lastAppliedToolSignature = signature;
|
|
2241
|
+
}
|
|
2218
2242
|
}
|
|
2219
2243
|
if (options?.persistMCPSelection !== false) {
|
|
2220
2244
|
this.#persistSelectedMCPToolNamesIfChanged(previousSelectedMCPToolNames);
|
|
@@ -2256,6 +2280,86 @@ export class AgentSession {
|
|
|
2256
2280
|
const activeToolNames = this.getActiveToolNames();
|
|
2257
2281
|
this.#baseSystemPrompt = await this.#rebuildSystemPrompt(activeToolNames, this.#toolRegistry);
|
|
2258
2282
|
this.agent.setSystemPrompt(this.#baseSystemPrompt);
|
|
2283
|
+
// Refresh the cached signature so a subsequent `#applyActiveToolsByName` with
|
|
2284
|
+
// the same tool set does not re-rebuild on top of the explicit refresh we
|
|
2285
|
+
// just performed (and conversely, a different set forces a fresh rebuild).
|
|
2286
|
+
const activeTools = activeToolNames
|
|
2287
|
+
.map(name => this.#toolRegistry.get(name))
|
|
2288
|
+
.filter((tool): tool is AgentTool => tool != null);
|
|
2289
|
+
this.#lastAppliedToolSignature = this.#computeAppliedToolSignature(activeToolNames, activeTools);
|
|
2290
|
+
}
|
|
2291
|
+
|
|
2292
|
+
/**
|
|
2293
|
+
* Compose a stable signature for the inputs that `rebuildSystemPrompt` reads.
|
|
2294
|
+
* Two calls producing identical signatures are guaranteed to produce identical
|
|
2295
|
+
* system prompt bytes, so the rebuild can be skipped.
|
|
2296
|
+
*
|
|
2297
|
+
* The signature covers:
|
|
2298
|
+
* 1. Active tool names in order (the prompt renders them in this order).
|
|
2299
|
+
* 2. Active tool labels, descriptions, and wire-visible names — all are
|
|
2300
|
+
* rendered into the prompt body (see `system-prompt.md` `{{label}}: \`{{name}}\``
|
|
2301
|
+
* and `toolPromptNames` in `buildSystemPrompt`). The wire name comes from
|
|
2302
|
+
* `tool.customWireName` and overrides the internal name on the model wire
|
|
2303
|
+
* (e.g. `edit` exposes itself as `apply_patch` to GPT-5 in apply_patch mode);
|
|
2304
|
+
* a stale wire name would desync prompt guidance from actual tool routing.
|
|
2305
|
+
* 3. When MCP discovery is on, every registry tool's name+label+description+
|
|
2306
|
+
* customWireName, since `rebuildSystemPrompt` summarizes discoverable MCP
|
|
2307
|
+
* tools that are not in the active set.
|
|
2308
|
+
* 4. MCP server instructions text (per server), since `rebuildSystemPrompt`
|
|
2309
|
+
* embeds these in the appended prompt under "## MCP Server Instructions".
|
|
2310
|
+
* A server upgrade can change instructions while keeping tools identical.
|
|
2311
|
+
*
|
|
2312
|
+
* Settings-driven tool metadata is covered automatically: built-in tools that
|
|
2313
|
+
* depend on settings expose `description`/`label` via getters (see `TaskTool`,
|
|
2314
|
+
* `SearchToolBm25Tool`, `EditTool`), and the signature reads them live on every
|
|
2315
|
+
* call - so a settings flip that mutates the rendered string differs the signature
|
|
2316
|
+
* the next time `#applyActiveToolsByName` runs. Do not refactor `describeTool` to
|
|
2317
|
+
* cache per-tool strings without preserving this property.
|
|
2318
|
+
*
|
|
2319
|
+
* Inputs NOT covered: tool input schemas; memory instructions read from disk;
|
|
2320
|
+
* and SDK-init-time closure constants in `sdk.ts` (`repeatToolDescriptions`,
|
|
2321
|
+
* `eagerTasks`, `intentField`, `mcpDiscoveryEnabled`, `secretsEnabled`). The
|
|
2322
|
+
* closure-captured ones cannot change at runtime regardless of skip behavior.
|
|
2323
|
+
* For everything else, callers must explicitly call `refreshBaseSystemPrompt()`
|
|
2324
|
+
* after side-effecting changes; see e.g. the memory hooks and
|
|
2325
|
+
* `#syncEditToolModeAfterModelChange`.
|
|
2326
|
+
*
|
|
2327
|
+
* The current calendar date IS covered (appended as a segment) because
|
|
2328
|
+
* `buildSystemPrompt` injects it into the prompt body (`Today is '{{date}}'`).
|
|
2329
|
+
* Without this, a session spanning midnight with only tool-stable MCP
|
|
2330
|
+
* reconnects would keep yesterday's date indefinitely.
|
|
2331
|
+
*/
|
|
2332
|
+
#computeAppliedToolSignature(toolNames: string[], tools: AgentTool[]): string {
|
|
2333
|
+
// Order-preserving join: any reorder must produce a different signature so
|
|
2334
|
+
// the rebuild fires and the new tool list reaches the API.
|
|
2335
|
+
const nameSegment = toolNames.join("\u0001");
|
|
2336
|
+
const describeTool = (tool: AgentTool): string =>
|
|
2337
|
+
`${tool.name}=${tool.label ?? ""}|${tool.description ?? ""}|${tool.customWireName ?? ""}`;
|
|
2338
|
+
const descriptionSegment = tools.map(describeTool).join("\u0002");
|
|
2339
|
+
let registrySegment = "";
|
|
2340
|
+
if (this.#mcpDiscoveryEnabled) {
|
|
2341
|
+
// Registry iteration order is not load-bearing for the prompt content, so we
|
|
2342
|
+
// sort to keep the signature insensitive to incidental insertion order.
|
|
2343
|
+
const entries: string[] = [];
|
|
2344
|
+
for (const tool of this.#toolRegistry.values()) {
|
|
2345
|
+
entries.push(describeTool(tool));
|
|
2346
|
+
}
|
|
2347
|
+
entries.sort();
|
|
2348
|
+
registrySegment = entries.join("\u0004");
|
|
2349
|
+
}
|
|
2350
|
+
let instructionsSegment = "";
|
|
2351
|
+
const serverInstructions = this.#getMcpServerInstructions?.();
|
|
2352
|
+
if (serverInstructions && serverInstructions.size > 0) {
|
|
2353
|
+
// Sort by server name so transport flap order does not perturb the signature.
|
|
2354
|
+
const entries: string[] = [];
|
|
2355
|
+
for (const [server, instructions] of serverInstructions) {
|
|
2356
|
+
entries.push(`${server}=${instructions}`);
|
|
2357
|
+
}
|
|
2358
|
+
entries.sort();
|
|
2359
|
+
instructionsSegment = entries.join("\u0006");
|
|
2360
|
+
}
|
|
2361
|
+
const date = new Date().toISOString().slice(0, 10);
|
|
2362
|
+
return `${nameSegment}\u0003${descriptionSegment}\u0005${registrySegment}\u0007${instructionsSegment}|${date}`;
|
|
2259
2363
|
}
|
|
2260
2364
|
|
|
2261
2365
|
/**
|
|
@@ -4248,9 +4352,10 @@ export class AgentSession {
|
|
|
4248
4352
|
}
|
|
4249
4353
|
|
|
4250
4354
|
// Start a new session
|
|
4355
|
+
const previousSessionFile = this.sessionFile;
|
|
4251
4356
|
await this.sessionManager.flush();
|
|
4252
4357
|
this.#asyncJobManager?.cancelAll();
|
|
4253
|
-
await this.sessionManager.newSession();
|
|
4358
|
+
await this.sessionManager.newSession(previousSessionFile ? { parentSession: previousSessionFile } : undefined);
|
|
4254
4359
|
this.agent.reset();
|
|
4255
4360
|
this.agent.sessionId = this.sessionManager.getSessionId();
|
|
4256
4361
|
this.#steeringMessages = [];
|
|
@@ -4262,6 +4367,7 @@ export class AgentSession {
|
|
|
4262
4367
|
// Inject the handoff document as a custom message
|
|
4263
4368
|
const handoffContent = `<handoff-context>\n${handoffText}\n</handoff-context>\n\nThe above is a handoff document from a previous session. Use this context to continue the work seamlessly.`;
|
|
4264
4369
|
this.sessionManager.appendCustomMessageEntry("handoff", handoffContent, true, undefined, "agent");
|
|
4370
|
+
await this.sessionManager.ensureOnDisk();
|
|
4265
4371
|
let savedPath: string | undefined;
|
|
4266
4372
|
if (options?.autoTriggered && this.settings.get("compaction.handoffSaveToDisk")) {
|
|
4267
4373
|
const artifactsDir = this.sessionManager.getArtifactsDir();
|
|
@@ -870,8 +870,8 @@ function sanitizeSessionName(value: string | undefined): string | undefined {
|
|
|
870
870
|
|
|
871
871
|
class RecentSessionInfo {
|
|
872
872
|
#fullName: string | undefined;
|
|
873
|
-
#name: string | undefined;
|
|
874
873
|
#timeAgo: string | undefined;
|
|
874
|
+
readonly #headerTimestamp: string | undefined;
|
|
875
875
|
|
|
876
876
|
constructor(
|
|
877
877
|
readonly path: string,
|
|
@@ -879,27 +879,31 @@ class RecentSessionInfo {
|
|
|
879
879
|
header: Record<string, unknown>,
|
|
880
880
|
firstPrompt?: string,
|
|
881
881
|
) {
|
|
882
|
-
//
|
|
882
|
+
// Prefer an explicit title, then the first user prompt. The raw UUID `id` is
|
|
883
|
+
// intentionally not used as a fallback: showing it as a "name" is unfriendly and
|
|
884
|
+
// indistinguishable from neighboring sessions in the UI. The friendly fallback is
|
|
885
|
+
// derived lazily in `fullName` from the session timestamp.
|
|
883
886
|
const trystr = (v: unknown) => (typeof v === "string" ? v : undefined);
|
|
884
|
-
this.#fullName =
|
|
885
|
-
|
|
886
|
-
sanitizeSessionName(firstPrompt) ??
|
|
887
|
-
sanitizeSessionName(trystr(header.id));
|
|
887
|
+
this.#fullName = sanitizeSessionName(trystr(header.title)) ?? sanitizeSessionName(firstPrompt);
|
|
888
|
+
this.#headerTimestamp = trystr(header.timestamp);
|
|
888
889
|
}
|
|
889
890
|
|
|
890
|
-
/**
|
|
891
|
+
/** Display name. Falls back to a timestamp-based label, never the raw UUID. */
|
|
891
892
|
get fullName(): string {
|
|
892
893
|
if (this.#fullName) return this.#fullName;
|
|
893
|
-
this.#
|
|
894
|
+
const ts = this.#headerTimestamp ? Date.parse(this.#headerTimestamp) : Number.NaN;
|
|
895
|
+
const date = new Date(Number.isFinite(ts) ? ts : this.mtime);
|
|
896
|
+
const time = date.toLocaleTimeString(undefined, { hour: "2-digit", minute: "2-digit" });
|
|
897
|
+
this.#fullName = `Untitled · ${time}`;
|
|
894
898
|
return this.#fullName;
|
|
895
899
|
}
|
|
896
900
|
|
|
897
|
-
/**
|
|
901
|
+
/**
|
|
902
|
+
* Display name without an arbitrary length cap. The renderer is responsible for
|
|
903
|
+
* width-aware truncation so adjacent fields (e.g. the relative time) stay visible.
|
|
904
|
+
*/
|
|
898
905
|
get name(): string {
|
|
899
|
-
|
|
900
|
-
const fullName = this.fullName;
|
|
901
|
-
this.#name = fullName.length <= 40 ? fullName : `${fullName.slice(0, 39)}…`;
|
|
902
|
-
return this.#name;
|
|
906
|
+
return this.fullName;
|
|
903
907
|
}
|
|
904
908
|
|
|
905
909
|
/** Human-readable relative time (e.g., "2 hours ago") */
|
package/src/task/agents.ts
CHANGED
|
@@ -69,10 +69,7 @@ const EMBEDDED_AGENT_DEFS: EmbeddedAgentDef[] = [
|
|
|
69
69
|
},
|
|
70
70
|
];
|
|
71
71
|
|
|
72
|
-
|
|
73
|
-
name: def.fileName,
|
|
74
|
-
content: buildAgentContent(def),
|
|
75
|
-
}));
|
|
72
|
+
// Computed lazily on first loadBundledAgents() call to avoid eager prompt.render at module load.
|
|
76
73
|
|
|
77
74
|
export class AgentParsingError extends Error {
|
|
78
75
|
constructor(
|
|
@@ -133,7 +130,9 @@ export function loadBundledAgents(): AgentDefinition[] {
|
|
|
133
130
|
if (bundledAgentsCache !== null) {
|
|
134
131
|
return bundledAgentsCache;
|
|
135
132
|
}
|
|
136
|
-
bundledAgentsCache =
|
|
133
|
+
bundledAgentsCache = EMBEDDED_AGENT_DEFS.map(def =>
|
|
134
|
+
parseAgent(`embedded:${def.fileName}`, buildAgentContent(def), "bundled"),
|
|
135
|
+
);
|
|
137
136
|
return bundledAgentsCache;
|
|
138
137
|
}
|
|
139
138
|
|
|
@@ -1,6 +1,11 @@
|
|
|
1
|
-
import { unzipSync } from "fflate";
|
|
2
1
|
import { ToolError } from "./tool-errors";
|
|
3
2
|
|
|
3
|
+
let fflateModulePromise: Promise<typeof import("fflate")> | undefined;
|
|
4
|
+
async function loadFflate(): Promise<typeof import("fflate")> {
|
|
5
|
+
if (!fflateModulePromise) fflateModulePromise = import("fflate");
|
|
6
|
+
return fflateModulePromise;
|
|
7
|
+
}
|
|
8
|
+
|
|
4
9
|
export type ArchiveFormat = "zip" | "tar" | "tar.gz";
|
|
5
10
|
|
|
6
11
|
export interface ArchivePathCandidate {
|
|
@@ -150,7 +155,8 @@ async function readTarEntries(bytes: Uint8Array): Promise<ArchiveIndexEntry[]> {
|
|
|
150
155
|
return entries;
|
|
151
156
|
}
|
|
152
157
|
|
|
153
|
-
function readZipEntries(bytes: Uint8Array): ArchiveIndexEntry[] {
|
|
158
|
+
async function readZipEntries(bytes: Uint8Array): Promise<ArchiveIndexEntry[]> {
|
|
159
|
+
const { unzipSync } = await loadFflate();
|
|
154
160
|
let files: Record<string, Uint8Array>;
|
|
155
161
|
try {
|
|
156
162
|
files = unzipSync(bytes);
|
|
@@ -310,6 +316,6 @@ export async function openArchive(filePath: string): Promise<ArchiveReader> {
|
|
|
310
316
|
}
|
|
311
317
|
|
|
312
318
|
const bytes = await Bun.file(filePath).bytes();
|
|
313
|
-
const entries = format === "zip" ? readZipEntries(bytes) : await readTarEntries(bytes);
|
|
319
|
+
const entries = format === "zip" ? await readZipEntries(bytes) : await readTarEntries(bytes);
|
|
314
320
|
return new ArchiveReader(format, entries);
|
|
315
321
|
}
|
package/src/tools/ast-edit.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
|
+
import * as path from "node:path";
|
|
1
2
|
import type { AgentTool, AgentToolContext, AgentToolResult, AgentToolUpdateCallback } from "@oh-my-pi/pi-agent-core";
|
|
2
|
-
import { type AstReplaceChange, astEdit } from "@oh-my-pi/pi-natives";
|
|
3
|
+
import { type AstReplaceChange, type AstReplaceFileChange, astEdit } from "@oh-my-pi/pi-natives";
|
|
3
4
|
import type { Component } from "@oh-my-pi/pi-tui";
|
|
4
5
|
import { Text } from "@oh-my-pi/pi-tui";
|
|
5
6
|
import { $envpos, prompt, untilAborted } from "@oh-my-pi/pi-utils";
|
|
@@ -19,7 +20,7 @@ import {
|
|
|
19
20
|
hasGlobPathChars,
|
|
20
21
|
normalizePathLikeInput,
|
|
21
22
|
parseSearchPath,
|
|
22
|
-
|
|
23
|
+
resolveExplicitSearchPaths,
|
|
23
24
|
resolveToCwd,
|
|
24
25
|
} from "./path-utils";
|
|
25
26
|
import {
|
|
@@ -46,12 +47,106 @@ const astEditSchema = Type.Object({
|
|
|
46
47
|
minItems: 1,
|
|
47
48
|
description: "rewrite ops",
|
|
48
49
|
}),
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
50
|
+
paths: Type.Array(Type.String({ description: "file, directory, glob, or internal URL to rewrite" }), {
|
|
51
|
+
minItems: 1,
|
|
52
|
+
description: "files, directories, globs, or internal URLs to rewrite",
|
|
53
|
+
examples: [["src/"], ["src/foo.ts"], ["src/**/*.ts"], ["src/", "packages/"]],
|
|
52
54
|
}),
|
|
53
55
|
});
|
|
54
56
|
|
|
57
|
+
interface AstEditCallOptions {
|
|
58
|
+
rewrites: Record<string, string>;
|
|
59
|
+
dryRun: boolean;
|
|
60
|
+
maxFiles: number;
|
|
61
|
+
failOnParseError: boolean;
|
|
62
|
+
signal?: AbortSignal;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
interface AstEditAggregatedResult {
|
|
66
|
+
changes: AstReplaceChange[];
|
|
67
|
+
fileChanges: AstReplaceFileChange[];
|
|
68
|
+
totalReplacements: number;
|
|
69
|
+
filesTouched: number;
|
|
70
|
+
filesSearched: number;
|
|
71
|
+
applied: boolean;
|
|
72
|
+
limitReached: boolean;
|
|
73
|
+
parseErrors?: string[];
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
async function runAstEditTargets(
|
|
77
|
+
targets: Array<{ basePath: string; glob?: string }>,
|
|
78
|
+
commonBasePath: string,
|
|
79
|
+
options: AstEditCallOptions,
|
|
80
|
+
): Promise<AstEditAggregatedResult> {
|
|
81
|
+
const aggregatedChanges: AstReplaceChange[] = [];
|
|
82
|
+
const fileCounts = new Map<string, number>();
|
|
83
|
+
const parseErrors: string[] = [];
|
|
84
|
+
let totalReplacements = 0;
|
|
85
|
+
let filesSearched = 0;
|
|
86
|
+
let limitReached = false;
|
|
87
|
+
let applied = !options.dryRun;
|
|
88
|
+
for (const target of targets) {
|
|
89
|
+
const targetResult = await astEdit({
|
|
90
|
+
rewrites: options.rewrites,
|
|
91
|
+
path: target.basePath,
|
|
92
|
+
glob: target.glob,
|
|
93
|
+
dryRun: options.dryRun,
|
|
94
|
+
maxFiles: options.maxFiles,
|
|
95
|
+
failOnParseError: options.failOnParseError,
|
|
96
|
+
signal: options.signal,
|
|
97
|
+
});
|
|
98
|
+
totalReplacements += targetResult.totalReplacements;
|
|
99
|
+
filesSearched += targetResult.filesSearched;
|
|
100
|
+
limitReached = limitReached || targetResult.limitReached;
|
|
101
|
+
applied = applied && targetResult.applied;
|
|
102
|
+
if (targetResult.parseErrors) parseErrors.push(...targetResult.parseErrors);
|
|
103
|
+
for (const change of targetResult.changes) {
|
|
104
|
+
const absolute = path.resolve(target.basePath, change.path);
|
|
105
|
+
const rebased = path.relative(commonBasePath, absolute).replace(/\\/g, "/");
|
|
106
|
+
aggregatedChanges.push({ ...change, path: rebased });
|
|
107
|
+
}
|
|
108
|
+
for (const fileChange of targetResult.fileChanges) {
|
|
109
|
+
const absolute = path.resolve(target.basePath, fileChange.path);
|
|
110
|
+
const rebased = path.relative(commonBasePath, absolute).replace(/\\/g, "/");
|
|
111
|
+
fileCounts.set(rebased, (fileCounts.get(rebased) ?? 0) + fileChange.count);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
const fileChanges: AstReplaceFileChange[] = Array.from(fileCounts, ([changePath, count]) => ({
|
|
115
|
+
path: changePath,
|
|
116
|
+
count,
|
|
117
|
+
}));
|
|
118
|
+
return {
|
|
119
|
+
changes: aggregatedChanges,
|
|
120
|
+
fileChanges,
|
|
121
|
+
totalReplacements,
|
|
122
|
+
filesTouched: fileChanges.length,
|
|
123
|
+
filesSearched,
|
|
124
|
+
applied,
|
|
125
|
+
limitReached,
|
|
126
|
+
parseErrors: parseErrors.length > 0 ? parseErrors : undefined,
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function runAstEditOnce(
|
|
131
|
+
targets: Array<{ basePath: string; glob?: string }> | undefined,
|
|
132
|
+
resolvedSearchPath: string,
|
|
133
|
+
globFilter: string | undefined,
|
|
134
|
+
options: AstEditCallOptions,
|
|
135
|
+
): Promise<AstEditAggregatedResult> {
|
|
136
|
+
if (targets) {
|
|
137
|
+
return runAstEditTargets(targets, resolvedSearchPath, options);
|
|
138
|
+
}
|
|
139
|
+
return astEdit({
|
|
140
|
+
rewrites: options.rewrites,
|
|
141
|
+
path: resolvedSearchPath,
|
|
142
|
+
glob: globFilter,
|
|
143
|
+
dryRun: options.dryRun,
|
|
144
|
+
maxFiles: options.maxFiles,
|
|
145
|
+
failOnParseError: options.failOnParseError,
|
|
146
|
+
signal: options.signal,
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
|
|
55
150
|
export interface AstEditToolDetails {
|
|
56
151
|
totalReplacements: number;
|
|
57
152
|
filesTouched: number;
|
|
@@ -107,40 +202,46 @@ export class AstEditTool implements AgentTool<typeof astEditSchema, AstEditToolD
|
|
|
107
202
|
const maxFiles = $envpos("PI_MAX_AST_FILES", 1000);
|
|
108
203
|
|
|
109
204
|
const formatScopePath = (targetPath: string): string => formatPathRelativeToCwd(targetPath, this.session.cwd);
|
|
110
|
-
let searchPath: string
|
|
111
|
-
let scopePath: string
|
|
205
|
+
let searchPath: string;
|
|
206
|
+
let scopePath: string;
|
|
112
207
|
let globFilter: string | undefined;
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
208
|
+
let multiTargets: Array<{ basePath: string; glob?: string }> | undefined;
|
|
209
|
+
const rawPaths = params.paths.map(normalizePathLikeInput);
|
|
210
|
+
if (rawPaths.some(rawPath => rawPath.length === 0)) {
|
|
211
|
+
throw new ToolError("`paths` must contain non-empty paths or globs");
|
|
116
212
|
}
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
213
|
+
const internalRouter = this.session.internalRouter;
|
|
214
|
+
const resolvedPathInputs: string[] = [];
|
|
215
|
+
for (const rawPath of rawPaths) {
|
|
216
|
+
if (!internalRouter?.canHandle(rawPath)) {
|
|
217
|
+
resolvedPathInputs.push(rawPath);
|
|
218
|
+
continue;
|
|
219
|
+
}
|
|
220
|
+
if (hasGlobPathChars(rawPath)) {
|
|
221
|
+
throw new ToolError(`Glob patterns are not supported for internal URLs: ${rawPath}`);
|
|
222
|
+
}
|
|
223
|
+
const resource = await internalRouter.resolve(rawPath);
|
|
224
|
+
if (!resource.sourcePath) {
|
|
225
|
+
throw new ToolError(`Cannot rewrite internal URL without backing file: ${rawPath}`);
|
|
226
|
+
}
|
|
227
|
+
resolvedPathInputs.push(resource.sourcePath);
|
|
228
|
+
}
|
|
229
|
+
if (resolvedPathInputs.length === 1) {
|
|
230
|
+
const parsedPath = parseSearchPath(resolvedPathInputs[0] ?? ".");
|
|
231
|
+
searchPath = resolveToCwd(parsedPath.basePath, this.session.cwd);
|
|
232
|
+
globFilter = parsedPath.glob;
|
|
233
|
+
scopePath = formatScopePath(searchPath);
|
|
234
|
+
} else {
|
|
235
|
+
const multiSearchPath = await resolveExplicitSearchPaths(resolvedPathInputs, this.session.cwd, globFilter);
|
|
236
|
+
if (!multiSearchPath) {
|
|
237
|
+
throw new ToolError("`paths` must contain at least one path or glob");
|
|
141
238
|
}
|
|
239
|
+
searchPath = multiSearchPath.basePath;
|
|
240
|
+
globFilter = multiSearchPath.targets ? undefined : multiSearchPath.glob;
|
|
241
|
+
multiTargets = multiSearchPath.targets;
|
|
242
|
+
scopePath = multiSearchPath.scopePath;
|
|
142
243
|
}
|
|
143
|
-
const resolvedSearchPath = searchPath
|
|
244
|
+
const resolvedSearchPath = searchPath;
|
|
144
245
|
scopePath = scopePath ?? formatScopePath(resolvedSearchPath);
|
|
145
246
|
let isDirectory: boolean;
|
|
146
247
|
try {
|
|
@@ -150,10 +251,8 @@ export class AstEditTool implements AgentTool<typeof astEditSchema, AstEditToolD
|
|
|
150
251
|
throw new ToolError(`Path not found: ${scopePath}`);
|
|
151
252
|
}
|
|
152
253
|
|
|
153
|
-
const result = await
|
|
254
|
+
const result = await runAstEditOnce(multiTargets, resolvedSearchPath, globFilter, {
|
|
154
255
|
rewrites: normalizedRewrites,
|
|
155
|
-
path: resolvedSearchPath,
|
|
156
|
-
glob: globFilter,
|
|
157
256
|
dryRun: true,
|
|
158
257
|
maxFiles,
|
|
159
258
|
failOnParseError: false,
|
|
@@ -256,7 +355,7 @@ export class AstEditTool implements AgentTool<typeof astEditSchema, AstEditToolD
|
|
|
256
355
|
count: fileReplacementCounts.get(filePath) ?? 0,
|
|
257
356
|
}));
|
|
258
357
|
if (result.limitReached) {
|
|
259
|
-
outputLines.push("", "Limit reached; narrow
|
|
358
|
+
outputLines.push("", "Limit reached; narrow paths.");
|
|
260
359
|
}
|
|
261
360
|
if (dedupedParseErrors.length) {
|
|
262
361
|
outputLines.push("", ...formatParseErrors(dedupedParseErrors));
|
|
@@ -270,10 +369,8 @@ export class AstEditTool implements AgentTool<typeof astEditSchema, AstEditToolD
|
|
|
270
369
|
label: `AST Edit: ${result.totalReplacements} replacement${previewReplacementPlural} in ${result.filesTouched} file${previewFilePlural}`,
|
|
271
370
|
sourceToolName: this.name,
|
|
272
371
|
apply: async (_reason: string) => {
|
|
273
|
-
const applyResult = await
|
|
372
|
+
const applyResult = await runAstEditOnce(multiTargets, resolvedSearchPath, globFilter, {
|
|
274
373
|
rewrites: normalizedRewrites,
|
|
275
|
-
path: resolvedSearchPath,
|
|
276
|
-
glob: globFilter,
|
|
277
374
|
dryRun: false,
|
|
278
375
|
maxFiles,
|
|
279
376
|
failOnParseError: false,
|
|
@@ -349,7 +446,7 @@ export class AstEditTool implements AgentTool<typeof astEditSchema, AstEditToolD
|
|
|
349
446
|
|
|
350
447
|
interface AstEditRenderArgs {
|
|
351
448
|
ops?: Array<{ pat?: string; out?: string }>;
|
|
352
|
-
|
|
449
|
+
paths?: string[];
|
|
353
450
|
}
|
|
354
451
|
|
|
355
452
|
const COLLAPSED_CHANGE_LIMIT = PREVIEW_LIMITS.COLLAPSED_LINES * 2;
|
|
@@ -358,7 +455,7 @@ export const astEditToolRenderer = {
|
|
|
358
455
|
inline: true,
|
|
359
456
|
renderCall(args: AstEditRenderArgs, _options: RenderResultOptions, uiTheme: Theme): Component {
|
|
360
457
|
const meta: string[] = [];
|
|
361
|
-
if (args.
|
|
458
|
+
if (args.paths?.length) meta.push(`in ${args.paths.join(", ")}`);
|
|
362
459
|
const rewriteCount = args.ops?.length ?? 0;
|
|
363
460
|
if (rewriteCount > 1) meta.push(`${rewriteCount} rewrites`);
|
|
364
461
|
|