@oh-my-pi/pi-coding-agent 14.4.1 → 14.4.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 +56 -0
- package/package.json +7 -7
- package/src/cli.ts +0 -1
- package/src/config/prompt-templates.ts +0 -30
- package/src/config/settings-schema.ts +68 -36
- package/src/config/settings.ts +1 -1
- package/src/edit/index.ts +1 -53
- package/src/edit/line-hash.ts +0 -53
- package/src/edit/modes/atom.ts +82 -47
- package/src/edit/modes/hashline.ts +6 -8
- package/src/edit/renderer.ts +6 -8
- package/src/edit/streaming.ts +90 -114
- package/src/export/html/template.generated.ts +1 -1
- package/src/export/html/template.js +10 -15
- package/src/internal-urls/docs-index.generated.ts +1 -2
- package/src/modes/components/session-observer-overlay.ts +635 -295
- package/src/modes/components/settings-defs.ts +1 -5
- package/src/modes/components/tool-execution.ts +2 -5
- package/src/modes/controllers/btw-controller.ts +17 -105
- package/src/modes/controllers/command-controller.ts +16 -5
- package/src/modes/controllers/selector-controller.ts +32 -19
- package/src/modes/controllers/todo-command-controller.ts +537 -0
- package/src/modes/interactive-mode.ts +45 -10
- package/src/modes/types.ts +3 -0
- package/src/modes/utils/ui-helpers.ts +17 -0
- package/src/prompts/system/irc-incoming.md +8 -0
- package/src/prompts/system/subagent-system-prompt.md +8 -0
- package/src/prompts/tools/ast-grep.md +1 -1
- package/src/prompts/tools/atom.md +37 -26
- package/src/prompts/tools/bash.md +2 -2
- package/src/prompts/tools/grep.md +2 -5
- package/src/prompts/tools/irc.md +49 -0
- package/src/prompts/tools/job.md +11 -0
- package/src/prompts/tools/read.md +12 -13
- package/src/prompts/tools/task.md +1 -1
- package/src/prompts/tools/todo-write.md +14 -5
- package/src/registry/agent-registry.ts +139 -0
- package/src/sdk.ts +35 -0
- package/src/session/agent-session.ts +226 -6
- package/src/session/session-manager.ts +13 -0
- package/src/session/session-storage.ts +4 -0
- package/src/session/streaming-output.ts +1 -1
- package/src/slash-commands/builtin-registry.ts +32 -0
- package/src/task/executor.ts +14 -0
- package/src/tools/bash.ts +1 -1
- package/src/tools/fetch.ts +18 -6
- package/src/tools/fs-cache-invalidation.ts +0 -5
- package/src/tools/grep.ts +4 -124
- package/src/tools/index.ts +12 -6
- package/src/tools/irc.ts +258 -0
- package/src/tools/job.ts +489 -0
- package/src/tools/match-line-format.ts +7 -6
- package/src/tools/output-meta.ts +1 -1
- package/src/tools/read.ts +36 -126
- package/src/tools/renderers.ts +2 -0
- package/src/tools/todo-write.ts +243 -12
- package/src/utils/edit-mode.ts +1 -2
- package/src/utils/file-display-mode.ts +0 -3
- package/src/web/search/index.ts +2 -2
- package/src/web/search/provider.ts +3 -0
- package/src/web/search/providers/searxng.ts +238 -0
- package/src/web/search/types.ts +3 -1
- package/src/cli/read-cli.ts +0 -67
- package/src/commands/read.ts +0 -33
- package/src/edit/modes/chunk.ts +0 -832
- package/src/prompts/tools/cancel-job.md +0 -5
- package/src/prompts/tools/chunk-edit.md +0 -158
- package/src/prompts/tools/poll.md +0 -5
- package/src/prompts/tools/read-chunk.md +0 -73
- package/src/tools/cancel-job.ts +0 -95
- package/src/tools/poll-tool.ts +0 -173
|
@@ -27,6 +27,7 @@ import {
|
|
|
27
27
|
} from "@oh-my-pi/pi-agent-core";
|
|
28
28
|
import type {
|
|
29
29
|
AssistantMessage,
|
|
30
|
+
Context,
|
|
30
31
|
Effort,
|
|
31
32
|
ImageContent,
|
|
32
33
|
Message,
|
|
@@ -48,6 +49,7 @@ import {
|
|
|
48
49
|
isUsageLimitError,
|
|
49
50
|
modelsAreEqual,
|
|
50
51
|
parseRateLimitReason,
|
|
52
|
+
streamSimple,
|
|
51
53
|
} from "@oh-my-pi/pi-ai";
|
|
52
54
|
import { killTree, MacOSPowerAssertion } from "@oh-my-pi/pi-natives";
|
|
53
55
|
import {
|
|
@@ -121,6 +123,7 @@ import type { PlanModeState } from "../plan-mode/state";
|
|
|
121
123
|
import autoHandoffThresholdFocusPrompt from "../prompts/system/auto-handoff-threshold-focus.md" with { type: "text" };
|
|
122
124
|
import eagerTodoPrompt from "../prompts/system/eager-todo.md" with { type: "text" };
|
|
123
125
|
import handoffDocumentPrompt from "../prompts/system/handoff-document.md" with { type: "text" };
|
|
126
|
+
import ircIncomingTemplate from "../prompts/system/irc-incoming.md" with { type: "text" };
|
|
124
127
|
import planModeActivePrompt from "../prompts/system/plan-mode-active.md" with { type: "text" };
|
|
125
128
|
import planModeReferencePrompt from "../prompts/system/plan-mode-reference.md" with { type: "text" };
|
|
126
129
|
import planModeToolDecisionReminderPrompt from "../prompts/system/plan-mode-tool-decision-reminder.md" with {
|
|
@@ -469,6 +472,11 @@ export class AgentSession {
|
|
|
469
472
|
#pendingPythonMessages: PythonExecutionMessage[] = [];
|
|
470
473
|
#activePythonExecutions = new Set<Promise<unknown>>();
|
|
471
474
|
#pythonExecutionDisposing = false;
|
|
475
|
+
|
|
476
|
+
// Background-channel IRC exchanges queued while the recipient was streaming.
|
|
477
|
+
// Drained into history (via emitExternalEvent) once the recipient becomes idle.
|
|
478
|
+
#pendingBackgroundExchanges: CustomMessage[][] = [];
|
|
479
|
+
#scheduledBackgroundExchangeFlush = false;
|
|
472
480
|
// Extension system
|
|
473
481
|
#extensionRunner: ExtensionRunner | undefined = undefined;
|
|
474
482
|
#turnIndex = 0;
|
|
@@ -2653,6 +2661,7 @@ export class AgentSession {
|
|
|
2653
2661
|
// Flush any pending bash messages before the new prompt
|
|
2654
2662
|
this.#flushPendingBashMessages();
|
|
2655
2663
|
this.#flushPendingPythonMessages();
|
|
2664
|
+
this.#flushPendingBackgroundExchanges();
|
|
2656
2665
|
|
|
2657
2666
|
// Reset todo reminder count on new user prompt
|
|
2658
2667
|
this.#todoReminderCount = 0;
|
|
@@ -3237,11 +3246,11 @@ export class AgentSession {
|
|
|
3237
3246
|
return phases.map(phase => ({
|
|
3238
3247
|
id: phase.id,
|
|
3239
3248
|
name: phase.name,
|
|
3240
|
-
tasks: phase.tasks.map(task =>
|
|
3241
|
-
id: task.id,
|
|
3242
|
-
|
|
3243
|
-
|
|
3244
|
-
})
|
|
3249
|
+
tasks: phase.tasks.map(task => {
|
|
3250
|
+
const out: TodoItem = { id: task.id, content: task.content, status: task.status };
|
|
3251
|
+
if (task.notes && task.notes.length > 0) out.notes = [...task.notes];
|
|
3252
|
+
return out;
|
|
3253
|
+
}),
|
|
3245
3254
|
}));
|
|
3246
3255
|
}
|
|
3247
3256
|
|
|
@@ -3364,7 +3373,15 @@ export class AgentSession {
|
|
|
3364
3373
|
this.#asyncJobManager?.cancelAll();
|
|
3365
3374
|
this.#closeAllProviderSessions("new session");
|
|
3366
3375
|
this.agent.reset();
|
|
3367
|
-
|
|
3376
|
+
if (options?.drop && previousSessionFile) {
|
|
3377
|
+
try {
|
|
3378
|
+
await this.sessionManager.dropSession(previousSessionFile);
|
|
3379
|
+
} catch (err) {
|
|
3380
|
+
logger.error("Failed to delete session during /drop", { err });
|
|
3381
|
+
}
|
|
3382
|
+
} else {
|
|
3383
|
+
await this.sessionManager.flush();
|
|
3384
|
+
}
|
|
3368
3385
|
await this.sessionManager.newSession(options);
|
|
3369
3386
|
this.setTodoPhases([]);
|
|
3370
3387
|
this.agent.sessionId = this.sessionManager.getSessionId();
|
|
@@ -5924,6 +5941,209 @@ export class AgentSession {
|
|
|
5924
5941
|
this.#pendingPythonMessages = [];
|
|
5925
5942
|
}
|
|
5926
5943
|
|
|
5944
|
+
// =========================================================================
|
|
5945
|
+
// Background-Channel IRC Exchanges
|
|
5946
|
+
// =========================================================================
|
|
5947
|
+
|
|
5948
|
+
/**
|
|
5949
|
+
* Generate an ephemeral reply to a background message (e.g. an IRC ping from
|
|
5950
|
+
* another agent) using this session's current model + system prompt + history.
|
|
5951
|
+
*
|
|
5952
|
+
* The reply is computed via a side-channel `streamSimple` call (analogous to
|
|
5953
|
+
* `/btw`) so it never blocks on the recipient's in-flight tool calls. After
|
|
5954
|
+
* the reply is generated, both the incoming question and the auto-reply are
|
|
5955
|
+
* queued for injection into the recipient's persisted history so the model
|
|
5956
|
+
* sees the exchange on its next turn. Injection happens immediately when the
|
|
5957
|
+
* session is idle, otherwise it is deferred until streaming ends.
|
|
5958
|
+
*/
|
|
5959
|
+
async respondAsBackground(args: {
|
|
5960
|
+
from: string;
|
|
5961
|
+
message: string;
|
|
5962
|
+
awaitReply?: boolean;
|
|
5963
|
+
signal?: AbortSignal;
|
|
5964
|
+
}): Promise<{ replyText: string | null }> {
|
|
5965
|
+
const awaitReply = args.awaitReply !== false;
|
|
5966
|
+
const incomingTimestamp = Date.now();
|
|
5967
|
+
const incomingRecord: CustomMessage = {
|
|
5968
|
+
role: "custom",
|
|
5969
|
+
customType: "irc:incoming",
|
|
5970
|
+
content: `[IRC \`${args.from}\` \u2192 you]\n\n${args.message}`,
|
|
5971
|
+
display: true,
|
|
5972
|
+
details: { from: args.from, message: args.message },
|
|
5973
|
+
attribution: "agent",
|
|
5974
|
+
timestamp: incomingTimestamp,
|
|
5975
|
+
};
|
|
5976
|
+
|
|
5977
|
+
if (!awaitReply) {
|
|
5978
|
+
this.#queueBackgroundExchangeInjection([incomingRecord]);
|
|
5979
|
+
return { replyText: null };
|
|
5980
|
+
}
|
|
5981
|
+
|
|
5982
|
+
const incomingPrompt = prompt.render(ircIncomingTemplate, {
|
|
5983
|
+
from: args.from,
|
|
5984
|
+
message: args.message,
|
|
5985
|
+
});
|
|
5986
|
+
const { replyText } = await this.runEphemeralTurn({
|
|
5987
|
+
promptText: incomingPrompt,
|
|
5988
|
+
signal: args.signal,
|
|
5989
|
+
});
|
|
5990
|
+
|
|
5991
|
+
const replyRecord: CustomMessage = {
|
|
5992
|
+
role: "custom",
|
|
5993
|
+
customType: "irc:autoreply",
|
|
5994
|
+
content: `[IRC you \u2192 \`${args.from}\` (auto)]\n\n${replyText}`,
|
|
5995
|
+
display: true,
|
|
5996
|
+
details: { to: args.from, reply: replyText },
|
|
5997
|
+
attribution: "agent",
|
|
5998
|
+
timestamp: Date.now(),
|
|
5999
|
+
};
|
|
6000
|
+
this.#queueBackgroundExchangeInjection([incomingRecord, replyRecord]);
|
|
6001
|
+
|
|
6002
|
+
return { replyText };
|
|
6003
|
+
}
|
|
6004
|
+
|
|
6005
|
+
/**
|
|
6006
|
+
* Run a single ephemeral side-channel turn against this session's current
|
|
6007
|
+
* model + system prompt + history. No tools are used; the side request
|
|
6008
|
+
* does not block on, or interfere with, any in-flight main turn. The
|
|
6009
|
+
* session's history and persisted state are NOT modified by this call.
|
|
6010
|
+
*
|
|
6011
|
+
* Used by `respondAsBackground` (IRC) and `BtwController` (`/btw`) to share
|
|
6012
|
+
* the snapshot + stream pipeline. The snapshot includes any in-flight
|
|
6013
|
+
* streaming assistant text so the model sees the half-finished response
|
|
6014
|
+
* rather than missing context.
|
|
6015
|
+
*/
|
|
6016
|
+
async runEphemeralTurn(args: {
|
|
6017
|
+
promptText: string;
|
|
6018
|
+
onTextDelta?: (delta: string) => void;
|
|
6019
|
+
signal?: AbortSignal;
|
|
6020
|
+
}): Promise<{ replyText: string; assistantMessage: AssistantMessage }> {
|
|
6021
|
+
const model = this.model;
|
|
6022
|
+
if (!model) {
|
|
6023
|
+
throw new Error("No active model on session");
|
|
6024
|
+
}
|
|
6025
|
+
const apiKey = await this.#modelRegistry.getApiKey(model, this.sessionId);
|
|
6026
|
+
if (!apiKey) {
|
|
6027
|
+
throw new Error(`No API key for ${model.provider}/${model.id}`);
|
|
6028
|
+
}
|
|
6029
|
+
|
|
6030
|
+
const snapshot = this.#buildEphemeralSnapshot(args.promptText);
|
|
6031
|
+
const llmMessages = await this.convertMessagesToLlm(snapshot, args.signal);
|
|
6032
|
+
const context: Context = {
|
|
6033
|
+
systemPrompt: this.systemPrompt,
|
|
6034
|
+
messages: llmMessages,
|
|
6035
|
+
};
|
|
6036
|
+
const options = this.prepareSimpleStreamOptions({
|
|
6037
|
+
apiKey,
|
|
6038
|
+
sessionId: this.sessionId,
|
|
6039
|
+
reasoning: toReasoningEffort(this.thinkingLevel),
|
|
6040
|
+
serviceTier: this.serviceTier,
|
|
6041
|
+
signal: args.signal,
|
|
6042
|
+
toolChoice: "none",
|
|
6043
|
+
});
|
|
6044
|
+
|
|
6045
|
+
let replyText = "";
|
|
6046
|
+
let assistantMessage: AssistantMessage | undefined;
|
|
6047
|
+
const stream = streamSimple(model, context, options);
|
|
6048
|
+
for await (const event of stream) {
|
|
6049
|
+
if (event.type === "text_delta") {
|
|
6050
|
+
replyText += event.delta;
|
|
6051
|
+
if (args.onTextDelta) args.onTextDelta(event.delta);
|
|
6052
|
+
continue;
|
|
6053
|
+
}
|
|
6054
|
+
if (event.type === "done") {
|
|
6055
|
+
assistantMessage = event.message;
|
|
6056
|
+
break;
|
|
6057
|
+
}
|
|
6058
|
+
if (event.type === "error") {
|
|
6059
|
+
throw new Error(event.error.errorMessage || "Ephemeral turn failed");
|
|
6060
|
+
}
|
|
6061
|
+
}
|
|
6062
|
+
|
|
6063
|
+
if (!assistantMessage) {
|
|
6064
|
+
throw new Error("Ephemeral turn ended without a final message");
|
|
6065
|
+
}
|
|
6066
|
+
return { replyText: replyText.trim(), assistantMessage };
|
|
6067
|
+
}
|
|
6068
|
+
|
|
6069
|
+
/**
|
|
6070
|
+
* Build a message snapshot for an ephemeral side-channel turn. Includes
|
|
6071
|
+
* the in-flight streaming assistant message (if any) so the model sees
|
|
6072
|
+
* the partial response in context, then appends the prompt as a virtual
|
|
6073
|
+
* user message.
|
|
6074
|
+
*/
|
|
6075
|
+
#buildEphemeralSnapshot(promptText: string): AgentMessage[] {
|
|
6076
|
+
const messages = [...this.messages];
|
|
6077
|
+
const streaming = this.agent.state.streamMessage;
|
|
6078
|
+
if (streaming && streaming.role === "assistant") {
|
|
6079
|
+
const streamingText = streaming.content
|
|
6080
|
+
.filter((c): c is TextContent => c.type === "text")
|
|
6081
|
+
.map(c => c.text)
|
|
6082
|
+
.join("");
|
|
6083
|
+
if (streamingText) {
|
|
6084
|
+
const normalized: AssistantMessage = {
|
|
6085
|
+
...streaming,
|
|
6086
|
+
content: [{ type: "text", text: streamingText }],
|
|
6087
|
+
};
|
|
6088
|
+
const lastMessage = messages.at(-1);
|
|
6089
|
+
if (lastMessage?.role === "assistant") {
|
|
6090
|
+
messages[messages.length - 1] = normalized;
|
|
6091
|
+
} else {
|
|
6092
|
+
messages.push(normalized);
|
|
6093
|
+
}
|
|
6094
|
+
}
|
|
6095
|
+
}
|
|
6096
|
+
messages.push({
|
|
6097
|
+
role: "user",
|
|
6098
|
+
content: [{ type: "text", text: promptText }],
|
|
6099
|
+
attribution: "agent",
|
|
6100
|
+
timestamp: Date.now(),
|
|
6101
|
+
});
|
|
6102
|
+
return messages;
|
|
6103
|
+
}
|
|
6104
|
+
|
|
6105
|
+
#queueBackgroundExchangeInjection(messages: CustomMessage[]): void {
|
|
6106
|
+
this.#pendingBackgroundExchanges.push(messages);
|
|
6107
|
+
if (!this.isStreaming) {
|
|
6108
|
+
this.#flushPendingBackgroundExchanges();
|
|
6109
|
+
return;
|
|
6110
|
+
}
|
|
6111
|
+
this.#scheduleBackgroundExchangeFlush();
|
|
6112
|
+
}
|
|
6113
|
+
|
|
6114
|
+
#scheduleBackgroundExchangeFlush(): void {
|
|
6115
|
+
if (this.#scheduledBackgroundExchangeFlush) return;
|
|
6116
|
+
this.#scheduledBackgroundExchangeFlush = true;
|
|
6117
|
+
const attempt = (): void => {
|
|
6118
|
+
if (this.#pendingBackgroundExchanges.length === 0) {
|
|
6119
|
+
this.#scheduledBackgroundExchangeFlush = false;
|
|
6120
|
+
return;
|
|
6121
|
+
}
|
|
6122
|
+
if (this.isStreaming) {
|
|
6123
|
+
setTimeout(attempt, 50);
|
|
6124
|
+
return;
|
|
6125
|
+
}
|
|
6126
|
+
this.#scheduledBackgroundExchangeFlush = false;
|
|
6127
|
+
this.#flushPendingBackgroundExchanges();
|
|
6128
|
+
};
|
|
6129
|
+
setTimeout(attempt, 0);
|
|
6130
|
+
}
|
|
6131
|
+
|
|
6132
|
+
#flushPendingBackgroundExchanges(): void {
|
|
6133
|
+
if (this.#pendingBackgroundExchanges.length === 0) return;
|
|
6134
|
+
const batches = this.#pendingBackgroundExchanges;
|
|
6135
|
+
this.#pendingBackgroundExchanges = [];
|
|
6136
|
+
for (const batch of batches) {
|
|
6137
|
+
for (const msg of batch) {
|
|
6138
|
+
// emitExternalEvent on message_end appends to agent state and dispatches
|
|
6139
|
+
// to all session listeners, which in turn handle TUI rendering and
|
|
6140
|
+
// sessionManager persistence via #handleAgentEvent.
|
|
6141
|
+
this.agent.emitExternalEvent({ type: "message_start", message: msg });
|
|
6142
|
+
this.agent.emitExternalEvent({ type: "message_end", message: msg });
|
|
6143
|
+
}
|
|
6144
|
+
}
|
|
6145
|
+
}
|
|
6146
|
+
|
|
5927
6147
|
// =========================================================================
|
|
5928
6148
|
// Session Management
|
|
5929
6149
|
// =========================================================================
|
|
@@ -66,6 +66,8 @@ export interface SessionHeader {
|
|
|
66
66
|
|
|
67
67
|
export interface NewSessionOptions {
|
|
68
68
|
parentSession?: string;
|
|
69
|
+
/** Skip flushing the current session and delete it instead of saving. */
|
|
70
|
+
drop?: boolean;
|
|
69
71
|
}
|
|
70
72
|
|
|
71
73
|
export interface SessionEntryBase {
|
|
@@ -1707,6 +1709,17 @@ export class SessionManager {
|
|
|
1707
1709
|
return this.#newSessionSync(options);
|
|
1708
1710
|
}
|
|
1709
1711
|
|
|
1712
|
+
/** Delete a session file and its artifacts. Drains the persist writer first to avoid EPERM on Windows. ENOENT is treated as success. */
|
|
1713
|
+
async dropSession(sessionPath: string): Promise<void> {
|
|
1714
|
+
await this.#closePersistWriter();
|
|
1715
|
+
try {
|
|
1716
|
+
await this.storage.deleteSessionWithArtifacts(sessionPath);
|
|
1717
|
+
} catch (err) {
|
|
1718
|
+
if (isEnoent(err)) return;
|
|
1719
|
+
throw err;
|
|
1720
|
+
}
|
|
1721
|
+
}
|
|
1722
|
+
|
|
1710
1723
|
/**
|
|
1711
1724
|
* Fork the current session, creating a new session file with the same entries.
|
|
1712
1725
|
* Returns both the old and new session file paths for artifact copying.
|
|
@@ -32,6 +32,7 @@ export interface SessionStorage {
|
|
|
32
32
|
writeText(path: string, content: string): Promise<void>;
|
|
33
33
|
rename(path: string, nextPath: string): Promise<void>;
|
|
34
34
|
unlink(path: string): Promise<void>;
|
|
35
|
+
deleteSessionWithArtifacts(sessionPath: string): Promise<void>;
|
|
35
36
|
openWriter(path: string, options?: { flags?: "a" | "w"; onError?: (err: Error) => void }): SessionStorageWriter;
|
|
36
37
|
}
|
|
37
38
|
|
|
@@ -360,6 +361,9 @@ export class MemorySessionStorage implements SessionStorage {
|
|
|
360
361
|
this.#files.delete(path);
|
|
361
362
|
return Promise.resolve();
|
|
362
363
|
}
|
|
364
|
+
deleteSessionWithArtifacts(_sessionPath: string): Promise<void> {
|
|
365
|
+
return Promise.resolve();
|
|
366
|
+
}
|
|
363
367
|
|
|
364
368
|
openWriter(path: string, options?: { flags?: "a" | "w"; onError?: (err: Error) => void }): SessionStorageWriter {
|
|
365
369
|
return new MemorySessionStorageWriter(this, path, options);
|
|
@@ -759,7 +759,7 @@ export function formatHeadTruncationNotice(
|
|
|
759
759
|
const totalFileLines = options.totalFileLines ?? truncation.totalLines;
|
|
760
760
|
const endLineDisplay = startLineDisplay + (truncation.outputLines ?? truncation.totalLines) - 1;
|
|
761
761
|
const nextOffset = endLineDisplay + 1;
|
|
762
|
-
const notice = `[Showing lines ${startLineDisplay}-${endLineDisplay} of ${totalFileLines}. Use sel
|
|
762
|
+
const notice = `[Showing lines ${startLineDisplay}-${endLineDisplay} of ${totalFileLines}. Use sel=${nextOffset} to continue]`;
|
|
763
763
|
return `\n\n${notice}`;
|
|
764
764
|
}
|
|
765
765
|
|
|
@@ -259,6 +259,30 @@ const BUILTIN_SLASH_COMMAND_REGISTRY: ReadonlyArray<BuiltinSlashCommandSpec> = [
|
|
|
259
259
|
runtime.ctx.editor.setText("");
|
|
260
260
|
},
|
|
261
261
|
},
|
|
262
|
+
{
|
|
263
|
+
name: "todo",
|
|
264
|
+
description: "View or modify the agent's todo list",
|
|
265
|
+
subcommands: [
|
|
266
|
+
{ name: "edit", description: "Open todos in $EDITOR (Markdown round-trip)" },
|
|
267
|
+
{ name: "copy", description: "Copy todos as Markdown to clipboard" },
|
|
268
|
+
{ name: "export", description: "Write todos as Markdown to a file (default: TODO.md)", usage: "[<path>]" },
|
|
269
|
+
{ name: "import", description: "Replace todos from a Markdown file (default: TODO.md)", usage: "[<path>]" },
|
|
270
|
+
{
|
|
271
|
+
name: "append",
|
|
272
|
+
description: "Append a task; phase fuzzy-matched or auto-created",
|
|
273
|
+
usage: "[<phase>] <task...>",
|
|
274
|
+
},
|
|
275
|
+
{ name: "start", description: "Mark task in_progress (fuzzy-matched)", usage: "<task>" },
|
|
276
|
+
{ name: "done", description: "Mark task/phase/all completed (fuzzy-matched)", usage: "[<task|phase>]" },
|
|
277
|
+
{ name: "drop", description: "Mark task/phase/all abandoned (fuzzy-matched)", usage: "[<task|phase>]" },
|
|
278
|
+
{ name: "rm", description: "Remove task/phase/all (fuzzy-matched)", usage: "[<task|phase>]" },
|
|
279
|
+
],
|
|
280
|
+
allowArgs: true,
|
|
281
|
+
handle: async (command, runtime) => {
|
|
282
|
+
await runtime.ctx.handleTodoCommand(command.args);
|
|
283
|
+
runtime.ctx.editor.setText("");
|
|
284
|
+
},
|
|
285
|
+
},
|
|
262
286
|
{
|
|
263
287
|
name: "session",
|
|
264
288
|
description: "Session management commands",
|
|
@@ -488,6 +512,14 @@ const BUILTIN_SLASH_COMMAND_REGISTRY: ReadonlyArray<BuiltinSlashCommandSpec> = [
|
|
|
488
512
|
await runtime.ctx.handleClearCommand();
|
|
489
513
|
},
|
|
490
514
|
},
|
|
515
|
+
{
|
|
516
|
+
name: "drop",
|
|
517
|
+
description: "Delete the current session and start a new one",
|
|
518
|
+
handle: async (_command, runtime) => {
|
|
519
|
+
runtime.ctx.editor.setText("");
|
|
520
|
+
await runtime.ctx.handleDropCommand();
|
|
521
|
+
},
|
|
522
|
+
},
|
|
491
523
|
{
|
|
492
524
|
name: "compact",
|
|
493
525
|
description: "Manually compact the session context",
|
package/src/task/executor.ts
CHANGED
|
@@ -20,6 +20,7 @@ import { callTool } from "../mcp/client";
|
|
|
20
20
|
import type { MCPManager } from "../mcp/manager";
|
|
21
21
|
import subagentSystemPromptTemplate from "../prompts/system/subagent-system-prompt.md" with { type: "text" };
|
|
22
22
|
import submitReminderTemplate from "../prompts/system/subagent-yield-reminder.md" with { type: "text" };
|
|
23
|
+
import { AgentRegistry } from "../registry/agent-registry";
|
|
23
24
|
import { createAgentSession, discoverAuthStorage } from "../sdk";
|
|
24
25
|
import type { AgentSession, AgentSessionEvent } from "../session/agent-session";
|
|
25
26
|
import type { AuthStorage } from "../session/auth-storage";
|
|
@@ -73,6 +74,14 @@ function normalizeModelPatterns(value: string | string[] | undefined): string[]
|
|
|
73
74
|
.filter(Boolean);
|
|
74
75
|
}
|
|
75
76
|
|
|
77
|
+
function renderIrcPeerRoster(selfId: string): string {
|
|
78
|
+
const peers = AgentRegistry.global()
|
|
79
|
+
.list()
|
|
80
|
+
.filter(ref => ref.id !== selfId && (ref.status === "running" || ref.status === "idle"));
|
|
81
|
+
if (peers.length === 0) return "- (no other live agents)";
|
|
82
|
+
return peers.map(peer => `- \`${peer.id}\` — ${peer.displayName} (${peer.kind}, ${peer.status})`).join("\n");
|
|
83
|
+
}
|
|
84
|
+
|
|
76
85
|
function withAbortTimeout<T>(promise: Promise<T>, timeoutMs: number, signal?: AbortSignal): Promise<T> {
|
|
77
86
|
if (signal?.aborted) {
|
|
78
87
|
return Promise.reject(new ToolAbortError());
|
|
@@ -543,6 +552,7 @@ export async function runSubprocess(options: ExecutorOptions): Promise<SingleRes
|
|
|
543
552
|
: agent.spawns.join(",");
|
|
544
553
|
|
|
545
554
|
const lspEnabled = enableLsp ?? true;
|
|
555
|
+
const ircEnabled = subagentSettings.get("irc.enabled") === true;
|
|
546
556
|
const skipPythonPreflight = Array.isArray(toolNames) && !toolNames.includes("python");
|
|
547
557
|
|
|
548
558
|
const outputChunks: string[] = [];
|
|
@@ -962,12 +972,16 @@ export async function runSubprocess(options: ExecutorOptions): Promise<SingleRes
|
|
|
962
972
|
worktree: worktree ?? "",
|
|
963
973
|
outputSchema: normalizedOutputSchema,
|
|
964
974
|
contextFile: options.contextFile,
|
|
975
|
+
ircPeers: ircEnabled ? renderIrcPeerRoster(id) : "",
|
|
976
|
+
ircSelfId: ircEnabled ? id : "",
|
|
965
977
|
}),
|
|
966
978
|
sessionManager,
|
|
967
979
|
hasUI: false,
|
|
968
980
|
spawns: spawnsEnv,
|
|
969
981
|
taskDepth: childDepth,
|
|
970
982
|
parentTaskPrefix: id,
|
|
983
|
+
agentId: id,
|
|
984
|
+
agentDisplayName: agent.name,
|
|
971
985
|
enableLsp: lspEnabled,
|
|
972
986
|
skipPythonPreflight,
|
|
973
987
|
enableMCP,
|
package/src/tools/bash.ts
CHANGED
|
@@ -346,7 +346,7 @@ export class BashTool implements AgentTool<BashToolSchema, BashToolDetails> {
|
|
|
346
346
|
}
|
|
347
347
|
lines.push(`Background job ${jobId} started: ${label}`);
|
|
348
348
|
lines.push("Result will be delivered automatically when complete.");
|
|
349
|
-
lines.push(`Use \`poll
|
|
349
|
+
lines.push(`Use \`job\` (with \`poll\` or \`cancel\`) or \`read jobs://${jobId}\` if needed.`);
|
|
350
350
|
return {
|
|
351
351
|
content: [{ type: "text", text: lines.join("\n") }],
|
|
352
352
|
details,
|
package/src/tools/fetch.ts
CHANGED
|
@@ -137,7 +137,9 @@ export function isReadableUrlPath(value: string): boolean {
|
|
|
137
137
|
return /^https?:\/\//i.test(value) || /^www\./i.test(value);
|
|
138
138
|
}
|
|
139
139
|
|
|
140
|
-
const URL_LINE_RANGE_RE = /^L(\d+)(
|
|
140
|
+
const URL_LINE_RANGE_RE = /^L?(\d+)(?:([-+])L?(\d+))?$/i;
|
|
141
|
+
// Embedded URL selectors (after a `:` in the path) keep the explicit `L` prefix to avoid colliding with ports such as `https://example.com:50`.
|
|
142
|
+
const URL_EMBEDDED_LINE_RANGE_RE = /^L\d+(?:[-+]L?\d+)?$/i;
|
|
141
143
|
|
|
142
144
|
export interface ParsedReadUrlTarget {
|
|
143
145
|
path: string;
|
|
@@ -159,11 +161,21 @@ export function parseReadUrlTarget(readPath: string, sel?: string): ParsedReadUr
|
|
|
159
161
|
if (lineMatch) {
|
|
160
162
|
const startLine = Number.parseInt(lineMatch[1]!, 10);
|
|
161
163
|
if (startLine < 1) {
|
|
162
|
-
throw new ToolError("
|
|
164
|
+
throw new ToolError("sel=0 is invalid; lines are 1-indexed. Use sel=1.");
|
|
163
165
|
}
|
|
164
|
-
const
|
|
165
|
-
|
|
166
|
-
|
|
166
|
+
const sep = lineMatch[2];
|
|
167
|
+
const rhs = lineMatch[3] ? Number.parseInt(lineMatch[3], 10) : undefined;
|
|
168
|
+
let endLine: number | undefined;
|
|
169
|
+
if (sep === "+") {
|
|
170
|
+
if (rhs === undefined || rhs < 1) {
|
|
171
|
+
throw new ToolError(`Invalid range ${startLine}+${rhs ?? 0}: count must be >= 1.`);
|
|
172
|
+
}
|
|
173
|
+
endLine = startLine + rhs - 1;
|
|
174
|
+
} else if (sep === "-") {
|
|
175
|
+
if (rhs === undefined || rhs < startLine) {
|
|
176
|
+
throw new ToolError(`Invalid range ${startLine}-${rhs ?? 0}: end must be >= start.`);
|
|
177
|
+
}
|
|
178
|
+
endLine = rhs;
|
|
167
179
|
}
|
|
168
180
|
return {
|
|
169
181
|
path: urlPath,
|
|
@@ -183,7 +195,7 @@ function tryExtractEmbeddedUrlSelector(readPath: string): { path: string; sel?:
|
|
|
183
195
|
}
|
|
184
196
|
|
|
185
197
|
const candidateSelector = readPath.slice(lastColonIndex + 1);
|
|
186
|
-
const isEmbeddedSelector = candidateSelector === "raw" ||
|
|
198
|
+
const isEmbeddedSelector = candidateSelector === "raw" || URL_EMBEDDED_LINE_RANGE_RE.test(candidateSelector);
|
|
187
199
|
if (!isEmbeddedSelector) {
|
|
188
200
|
return null;
|
|
189
201
|
}
|
|
@@ -1,12 +1,10 @@
|
|
|
1
1
|
import { invalidateFsScanCache } from "@oh-my-pi/pi-natives";
|
|
2
|
-
import { invalidateChunkCache } from "../edit/modes/chunk";
|
|
3
2
|
|
|
4
3
|
/**
|
|
5
4
|
* Invalidate shared filesystem scan caches after a content write/update.
|
|
6
5
|
*/
|
|
7
6
|
export function invalidateFsScanAfterWrite(path: string): void {
|
|
8
7
|
invalidateFsScanCache(path);
|
|
9
|
-
invalidateChunkCache(path);
|
|
10
8
|
}
|
|
11
9
|
|
|
12
10
|
/**
|
|
@@ -14,7 +12,6 @@ export function invalidateFsScanAfterWrite(path: string): void {
|
|
|
14
12
|
*/
|
|
15
13
|
export function invalidateFsScanAfterDelete(path: string): void {
|
|
16
14
|
invalidateFsScanCache(path);
|
|
17
|
-
invalidateChunkCache(path);
|
|
18
15
|
}
|
|
19
16
|
|
|
20
17
|
/**
|
|
@@ -25,9 +22,7 @@ export function invalidateFsScanAfterDelete(path: string): void {
|
|
|
25
22
|
*/
|
|
26
23
|
export function invalidateFsScanAfterRename(oldPath: string, newPath: string): void {
|
|
27
24
|
invalidateFsScanCache(oldPath);
|
|
28
|
-
invalidateChunkCache(oldPath);
|
|
29
25
|
if (newPath !== oldPath) {
|
|
30
26
|
invalidateFsScanCache(newPath);
|
|
31
|
-
invalidateChunkCache(newPath);
|
|
32
27
|
}
|
|
33
28
|
}
|