@oh-my-pi/pi-coding-agent 16.4.4 → 16.4.5
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 +33 -0
- package/dist/cli.js +3563 -3540
- package/dist/types/async/job-manager.d.ts +8 -0
- package/dist/types/config/settings-schema.d.ts +18 -8
- package/dist/types/config/settings.d.ts +3 -2
- package/dist/types/discovery/helpers.d.ts +2 -2
- package/dist/types/extensibility/extensions/types.d.ts +36 -0
- package/dist/types/irc/bus.d.ts +4 -0
- package/dist/types/modes/components/__tests__/pause-screen.test.d.ts +1 -0
- package/dist/types/modes/components/ask-dialog.d.ts +27 -0
- package/dist/types/modes/components/index.d.ts +2 -1
- package/dist/types/modes/components/model-browser.d.ts +97 -0
- package/dist/types/modes/components/model-hub.d.ts +51 -0
- package/dist/types/modes/components/pause-screen.d.ts +43 -0
- package/dist/types/modes/components/session-selector.d.ts +13 -0
- package/dist/types/modes/components/tool-execution.d.ts +2 -0
- package/dist/types/modes/controllers/extension-ui-controller.d.ts +2 -1
- package/dist/types/modes/interactive-mode.d.ts +1 -0
- package/dist/types/modes/shared.d.ts +1 -1
- package/dist/types/modes/types.d.ts +2 -0
- package/dist/types/session/agent-session.d.ts +1 -1
- package/dist/types/session/session-context.d.ts +9 -0
- package/dist/types/task/executor.d.ts +26 -13
- package/dist/types/task/index.d.ts +12 -11
- package/dist/types/task/label.d.ts +4 -0
- package/dist/types/task/repair-args.d.ts +8 -8
- package/dist/types/task/types.d.ts +31 -56
- package/dist/types/tools/ask.d.ts +12 -0
- package/dist/types/tools/conflict-detect.d.ts +17 -1
- package/dist/types/tools/job.d.ts +16 -0
- package/package.json +12 -12
- package/src/async/job-manager.ts +9 -0
- package/src/commit/agentic/tools/analyze-file.ts +2 -3
- package/src/config/settings-schema.ts +17 -6
- package/src/config/settings.ts +13 -4
- package/src/discovery/helpers.ts +3 -4
- package/src/extensibility/custom-tools/loader.ts +70 -37
- package/src/extensibility/extensions/types.ts +46 -0
- package/src/irc/bus.ts +61 -20
- package/src/modes/components/__tests__/pause-screen.test.ts +143 -0
- package/src/modes/components/advisor-config.ts +30 -22
- package/src/modes/components/ask-dialog.ts +888 -0
- package/src/modes/components/index.ts +2 -1
- package/src/modes/components/model-browser.ts +688 -0
- package/src/modes/components/model-hub.ts +1655 -0
- package/src/modes/components/pause-screen.ts +208 -0
- package/src/modes/components/session-selector.ts +299 -42
- package/src/modes/components/tool-execution.ts +2 -0
- package/src/modes/controllers/event-controller.ts +8 -2
- package/src/modes/controllers/extension-ui-controller.ts +252 -5
- package/src/modes/controllers/selector-controller.ts +143 -87
- package/src/modes/controllers/tan-command-controller.ts +1 -1
- package/src/modes/interactive-mode.ts +3 -0
- package/src/modes/shared.ts +1 -1
- package/src/modes/theme/theme.ts +3 -3
- package/src/modes/types.ts +2 -0
- package/src/modes/utils/ui-helpers.ts +31 -4
- package/src/prompts/agents/scout.md +0 -1
- package/src/prompts/agents/task.md +1 -1
- package/src/prompts/system/subagent-system-prompt.md +1 -5
- package/src/prompts/system/subagent-yield-reminder.md +10 -0
- package/src/prompts/system/task-label.md +23 -0
- package/src/prompts/tools/job.md +1 -1
- package/src/prompts/tools/task-summary.md +3 -0
- package/src/prompts/tools/task.md +17 -18
- package/src/session/agent-session.ts +2 -1
- package/src/session/session-context.test.ts +73 -0
- package/src/session/session-context.ts +43 -26
- package/src/slash-commands/builtin-registry.ts +9 -0
- package/src/task/agents.ts +2 -0
- package/src/task/executor.ts +159 -46
- package/src/task/index.ts +377 -239
- package/src/task/label.ts +38 -0
- package/src/task/render.ts +74 -22
- package/src/task/repair-args.ts +20 -31
- package/src/task/spawn-policy.test.ts +4 -4
- package/src/task/types.ts +46 -66
- package/src/tools/ask.ts +233 -40
- package/src/tools/conflict-detect.ts +102 -5
- package/src/tools/index.ts +1 -0
- package/src/tools/irc.ts +20 -11
- package/src/tools/job.ts +158 -18
- package/src/tools/write.ts +70 -6
- package/src/vibe/runtime.ts +1 -1
- package/src/web/search/providers/browser-headers.ts +30 -13
- package/dist/types/modes/components/model-selector.d.ts +0 -37
- package/dist/types/tools/bash-command-fixup.d.ts +0 -3
- package/src/modes/components/model-selector.ts +0 -1291
- package/src/tools/bash-command-fixup.ts +0 -4
package/src/tools/irc.ts
CHANGED
|
@@ -386,20 +386,29 @@ export class IrcTool implements AgentTool<typeof ircSchema, IrcDetails> {
|
|
|
386
386
|
};
|
|
387
387
|
}
|
|
388
388
|
const timeoutMs = this.#resolveTimeoutMs(params);
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
389
|
+
try {
|
|
390
|
+
const waited = await IrcBus.global().wait(senderId, { from }, timeoutMs, signal, {
|
|
391
|
+
liveness: { registry, senderId },
|
|
392
|
+
});
|
|
393
|
+
if (!waited) {
|
|
394
|
+
const filterNote = from ? ` from ${from}` : "";
|
|
395
|
+
return {
|
|
396
|
+
content: [{ type: "text", text: `No message${filterNote} within ${formatDuration(timeoutMs)}.` }],
|
|
397
|
+
details: { op: "wait", from: senderId, waited: null },
|
|
398
|
+
// A clean wait timeout carries no information once consumed.
|
|
399
|
+
useless: true,
|
|
400
|
+
};
|
|
401
|
+
}
|
|
392
402
|
return {
|
|
393
|
-
content: [{ type: "text", text:
|
|
394
|
-
details: { op: "wait", from: senderId, waited
|
|
395
|
-
// A clean wait timeout carries no information once consumed.
|
|
396
|
-
useless: true,
|
|
403
|
+
content: [{ type: "text", text: formatIncoming(waited) }],
|
|
404
|
+
details: { op: "wait", from: senderId, waited },
|
|
397
405
|
};
|
|
406
|
+
} catch (error) {
|
|
407
|
+
if (signal?.aborted) {
|
|
408
|
+
throw error;
|
|
409
|
+
}
|
|
410
|
+
return errorResult(error instanceof Error ? error.message : String(error), { op: "wait", from: senderId });
|
|
398
411
|
}
|
|
399
|
-
return {
|
|
400
|
-
content: [{ type: "text", text: formatIncoming(waited) }],
|
|
401
|
-
details: { op: "wait", from: senderId, waited },
|
|
402
|
-
};
|
|
403
412
|
}
|
|
404
413
|
|
|
405
414
|
#executeInbox(registry: AgentRegistry, senderId: string, params: IrcParams): AgentToolResult<IrcDetails> {
|
package/src/tools/job.ts
CHANGED
|
@@ -61,9 +61,26 @@ interface CancelOutcome {
|
|
|
61
61
|
message: string;
|
|
62
62
|
}
|
|
63
63
|
|
|
64
|
+
/**
|
|
65
|
+
* A live subagent from the AgentRegistry that has no backing job in the
|
|
66
|
+
* AsyncJobManager — e.g. an idle agent woken (or a parked agent revived) via
|
|
67
|
+
* `irc`, or a spawn owned by another agent. Surfaced by `list` and empty-poll
|
|
68
|
+
* snapshots so the job tool's picture matches the UI's running-agent count.
|
|
69
|
+
*/
|
|
70
|
+
interface AgentActivitySnapshot {
|
|
71
|
+
id: string;
|
|
72
|
+
parentId?: string;
|
|
73
|
+
/** Latest activity gist recorded by the registry (display-only). */
|
|
74
|
+
activity?: string;
|
|
75
|
+
/** Time since the agent was registered. */
|
|
76
|
+
ageMs: number;
|
|
77
|
+
}
|
|
78
|
+
|
|
64
79
|
export interface JobToolDetails {
|
|
65
80
|
jobs: JobSnapshot[];
|
|
66
81
|
cancelled?: { id: string; status: CancelStatus }[];
|
|
82
|
+
/** Running subagents not represented by a job row in this result. */
|
|
83
|
+
agents?: AgentActivitySnapshot[];
|
|
67
84
|
}
|
|
68
85
|
|
|
69
86
|
/**
|
|
@@ -118,7 +135,9 @@ export class JobTool implements AgentTool<typeof jobSchema, JobToolDetails> {
|
|
|
118
135
|
if (params.cancel?.length || params.poll?.length) {
|
|
119
136
|
throw new ToolError("`list` cannot be combined with `poll` or `cancel`.");
|
|
120
137
|
}
|
|
121
|
-
|
|
138
|
+
const jobs = manager.getAllJobs(ownerFilter);
|
|
139
|
+
const agents = this.#runningAgentsOutsideJobs();
|
|
140
|
+
return this.#buildResult(manager, jobs, [], agents);
|
|
122
141
|
}
|
|
123
142
|
|
|
124
143
|
const cancelIds = params.cancel ?? [];
|
|
@@ -166,15 +185,37 @@ export class JobTool implements AgentTool<typeof jobSchema, JobToolDetails> {
|
|
|
166
185
|
const cancelledJobs = this.#visibleJobs(manager, cancelIds, ownerId);
|
|
167
186
|
return this.#buildResult(manager, cancelledJobs, cancelOutcomes);
|
|
168
187
|
}
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
188
|
+
// Zero pollable jobs is not necessarily "nothing running": agents
|
|
189
|
+
// woken via irc or owned by another agent run with no job entry.
|
|
190
|
+
// Report them so the snapshot matches the UI's running-agent count
|
|
191
|
+
// (task job ids are agent ids, so a stale poll id often names one).
|
|
192
|
+
const agents = this.#runningAgentsOutsideJobs();
|
|
193
|
+
const lines: string[] = [];
|
|
194
|
+
if (requestedPollIds?.length) {
|
|
195
|
+
lines.push(`No matching jobs found for IDs: ${requestedPollIds.join(", ")}`);
|
|
196
|
+
const registry = this.session.agentRegistry;
|
|
197
|
+
for (const id of requestedPollIds) {
|
|
198
|
+
const ref = registry?.get(id);
|
|
199
|
+
if (!ref) continue;
|
|
200
|
+
lines.push(
|
|
201
|
+
ref.status === "running"
|
|
202
|
+
? `- \`${id}\` is a running agent with no job entry — coordinate via \`irc\`; transcript at history://${id}`
|
|
203
|
+
: `- \`${id}\` is a ${ref.status} agent (its job is gone) — transcript at history://${id}`,
|
|
204
|
+
);
|
|
205
|
+
}
|
|
206
|
+
} else {
|
|
207
|
+
lines.push("No running background jobs to wait for.");
|
|
208
|
+
}
|
|
209
|
+
if (agents.length > 0) {
|
|
210
|
+
lines.push("", ...this.#describeAgents(agents));
|
|
211
|
+
}
|
|
172
212
|
return {
|
|
173
|
-
content: [{ type: "text", text:
|
|
174
|
-
details: { jobs: [] },
|
|
213
|
+
content: [{ type: "text", text: lines.join("\n") }],
|
|
214
|
+
details: { jobs: [], ...(agents.length ? { agents } : {}) },
|
|
175
215
|
// Nothing found / nothing to wait for is noise once consumed —
|
|
176
|
-
// the follow-up call has already corrected course.
|
|
177
|
-
|
|
216
|
+
// the follow-up call has already corrected course. Running agents
|
|
217
|
+
// are real state the model may act on, so keep those results.
|
|
218
|
+
...(agents.length === 0 ? { useless: true } : {}),
|
|
178
219
|
};
|
|
179
220
|
}
|
|
180
221
|
|
|
@@ -266,6 +307,57 @@ export class JobTool implements AgentTool<typeof jobSchema, JobToolDetails> {
|
|
|
266
307
|
return out;
|
|
267
308
|
}
|
|
268
309
|
|
|
310
|
+
/**
|
|
311
|
+
* Running subagents from the registry that are not covered by one of the
|
|
312
|
+
* caller's running jobs. Agents woken via `irc` (idle wake / park revival)
|
|
313
|
+
* and spawns owned by another agent run with no AsyncJobManager entry, yet
|
|
314
|
+
* the UI's agent badge counts them — a snapshot must account for that
|
|
315
|
+
* activity instead of implying the system is quiet. Existence is already
|
|
316
|
+
* public via the `irc` roster, so listing ids here leaks nothing new; job
|
|
317
|
+
* *control* stays owner-scoped.
|
|
318
|
+
*/
|
|
319
|
+
#runningAgentsOutsideJobs(): AgentActivitySnapshot[] {
|
|
320
|
+
const registry = this.session.agentRegistry;
|
|
321
|
+
if (!registry) return [];
|
|
322
|
+
const selfId = this.session.getAgentId?.() ?? undefined;
|
|
323
|
+
// Cover = the caller's RUNNING jobs only. A settled job still sitting in
|
|
324
|
+
// delivery retention must not hide its agent if that agent was re-woken
|
|
325
|
+
// (e.g. via irc) and is running again without a job.
|
|
326
|
+
const covered = new Set<string>();
|
|
327
|
+
const manager = this.session.asyncJobManager;
|
|
328
|
+
if (manager) {
|
|
329
|
+
for (const job of manager.getRunningJobs(selfId ? { ownerId: selfId } : undefined)) {
|
|
330
|
+
covered.add(job.id);
|
|
331
|
+
if (job.agentId) covered.add(job.agentId);
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
const now = Date.now();
|
|
335
|
+
const out: AgentActivitySnapshot[] = [];
|
|
336
|
+
for (const ref of registry.list()) {
|
|
337
|
+
if (ref.kind !== "sub" || ref.status !== "running") continue;
|
|
338
|
+
if (ref.id === selfId || covered.has(ref.id)) continue;
|
|
339
|
+
out.push({
|
|
340
|
+
id: ref.id,
|
|
341
|
+
...(ref.parentId ? { parentId: ref.parentId } : {}),
|
|
342
|
+
...(ref.activity ? { activity: ref.activity } : {}),
|
|
343
|
+
ageMs: Math.max(0, now - ref.createdAt),
|
|
344
|
+
});
|
|
345
|
+
}
|
|
346
|
+
return out;
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
/** Model-facing lines for the running-agents section shared by `list` and empty-poll results. */
|
|
350
|
+
#describeAgents(agents: AgentActivitySnapshot[]): string[] {
|
|
351
|
+
const lines = [`## Running Agents (${agents.length}) — not job-backed\n`];
|
|
352
|
+
for (const agent of agents) {
|
|
353
|
+
const parent = agent.parentId ? ` (spawned by \`${agent.parentId}\`)` : "";
|
|
354
|
+
const activity = agent.activity ? ` — ${agent.activity}` : "";
|
|
355
|
+
lines.push(`- \`${agent.id}\`${parent} — up ${formatDuration(agent.ageMs)}${activity}`);
|
|
356
|
+
}
|
|
357
|
+
lines.push("", "These agents have no job entry; coordinate via `irc`, transcripts at `history://<id>`.");
|
|
358
|
+
return lines;
|
|
359
|
+
}
|
|
360
|
+
|
|
269
361
|
#snapshotJobs(
|
|
270
362
|
jobs: {
|
|
271
363
|
id: string;
|
|
@@ -305,6 +397,7 @@ export class JobTool implements AgentTool<typeof jobSchema, JobToolDetails> {
|
|
|
305
397
|
errorText?: string;
|
|
306
398
|
}[],
|
|
307
399
|
cancelOutcomes: CancelOutcome[],
|
|
400
|
+
agents: AgentActivitySnapshot[] = [],
|
|
308
401
|
): AgentToolResult<JobToolDetails> {
|
|
309
402
|
// Deduplicate by id (cancelled jobs may also appear in the watched set).
|
|
310
403
|
const seen = new Set<string>();
|
|
@@ -350,9 +443,21 @@ export class JobTool implements AgentTool<typeof jobSchema, JobToolDetails> {
|
|
|
350
443
|
}
|
|
351
444
|
}
|
|
352
445
|
|
|
446
|
+
if (agents.length > 0) {
|
|
447
|
+
if (lines.length > 0) lines.push("");
|
|
448
|
+
lines.push(...this.#describeAgents(agents));
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
// A tool result must never be empty text — the model cannot tell "no
|
|
452
|
+
// jobs" from a malfunction (reported exactly that way in QA).
|
|
453
|
+
if (lines.length === 0) {
|
|
454
|
+
lines.push("No background jobs.");
|
|
455
|
+
}
|
|
456
|
+
|
|
353
457
|
const details: JobToolDetails = {
|
|
354
458
|
jobs: jobResults,
|
|
355
459
|
...(cancelOutcomes.length ? { cancelled: cancelOutcomes.map(({ id, status }) => ({ id, status })) } : {}),
|
|
460
|
+
...(agents.length ? { agents } : {}),
|
|
356
461
|
};
|
|
357
462
|
return {
|
|
358
463
|
content: [{ type: "text", text: lines.join("\n").trimEnd() }],
|
|
@@ -463,8 +568,9 @@ export const jobToolRenderer = {
|
|
|
463
568
|
args?: JobRenderArgs,
|
|
464
569
|
): Component {
|
|
465
570
|
let jobs = result.details?.jobs ?? [];
|
|
571
|
+
const agents = result.details?.agents ?? [];
|
|
466
572
|
|
|
467
|
-
if (jobs.length === 0) {
|
|
573
|
+
if (jobs.length === 0 && agents.length === 0) {
|
|
468
574
|
const fallback = result.content?.find(c => c.type === "text")?.text || "No jobs to process";
|
|
469
575
|
const header = renderStatusLine({ icon: "warning", title: describeTarget(args) || "Job" }, uiTheme);
|
|
470
576
|
return new Text([header, formatEmptyMessage(fallback, uiTheme)].join("\n"), 0, 0);
|
|
@@ -474,7 +580,10 @@ export const jobToolRenderer = {
|
|
|
474
580
|
? !args.list && (!args.cancel || args.cancel.length === 0 || args.poll !== undefined)
|
|
475
581
|
: true;
|
|
476
582
|
|
|
477
|
-
|
|
583
|
+
// Agent-carrying results (list / empty-poll roster) are real snapshots,
|
|
584
|
+
// not displaceable waiting frames — only agentless polls collapse their
|
|
585
|
+
// still-running rows once sealed.
|
|
586
|
+
if (!options.isPartial && isPollCall && agents.length === 0) {
|
|
478
587
|
jobs = jobs.filter(job => job.status !== "running");
|
|
479
588
|
if (jobs.length === 0) {
|
|
480
589
|
return new Text("", 0, 0);
|
|
@@ -490,20 +599,26 @@ export const jobToolRenderer = {
|
|
|
490
599
|
if (counts.completed > 0) meta.push(uiTheme.fg("success", `${counts.completed} done`));
|
|
491
600
|
if (counts.failed > 0) meta.push(uiTheme.fg("error", `${counts.failed} failed`));
|
|
492
601
|
if (counts.cancelled > 0) meta.push(uiTheme.fg("warning", `${counts.cancelled} cancelled`));
|
|
602
|
+
if (agents.length > 0 && jobs.length > 0) {
|
|
603
|
+
meta.push(uiTheme.fg("accent", `${agents.length} agent${agents.length === 1 ? "" : "s"}`));
|
|
604
|
+
}
|
|
493
605
|
|
|
494
|
-
const headerIcon: ToolUIStatus =
|
|
606
|
+
const headerIcon: ToolUIStatus =
|
|
607
|
+
counts.failed > 0 ? "warning" : counts.running > 0 || agents.length > 0 ? "info" : "success";
|
|
495
608
|
const jobsNoun = jobs.length === 1 ? "job" : "jobs";
|
|
496
609
|
const description =
|
|
497
|
-
|
|
498
|
-
?
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
610
|
+
jobs.length === 0
|
|
611
|
+
? `${agents.length} running agent${agents.length === 1 ? "" : "s"} — no jobs`
|
|
612
|
+
: counts.running > 0
|
|
613
|
+
? counts.running === jobs.length
|
|
614
|
+
? `waiting on ${jobs.length} ${jobsNoun}`
|
|
615
|
+
: `waiting on ${counts.running} of ${jobs.length} ${jobsNoun}`
|
|
616
|
+
: `${jobs.length} ${jobsNoun} settled`;
|
|
502
617
|
|
|
503
618
|
const header = renderStatusLine(
|
|
504
619
|
{
|
|
505
620
|
icon: headerIcon,
|
|
506
|
-
spinnerFrame: counts.running > 0 ? options.spinnerFrame : undefined,
|
|
621
|
+
spinnerFrame: counts.running > 0 || agents.length > 0 ? options.spinnerFrame : undefined,
|
|
507
622
|
title: description,
|
|
508
623
|
meta,
|
|
509
624
|
},
|
|
@@ -598,7 +713,32 @@ export const jobToolRenderer = {
|
|
|
598
713
|
uiTheme,
|
|
599
714
|
);
|
|
600
715
|
|
|
601
|
-
|
|
716
|
+
// Agents run outside job control; render them as their own tree so
|
|
717
|
+
// they never skew the job counts or the "waiting on N jobs" title.
|
|
718
|
+
const agentLines =
|
|
719
|
+
agents.length === 0
|
|
720
|
+
? []
|
|
721
|
+
: renderTreeList<AgentActivitySnapshot>(
|
|
722
|
+
{
|
|
723
|
+
items: agents,
|
|
724
|
+
expanded,
|
|
725
|
+
maxCollapsed: COLLAPSED_LIST_LIMIT,
|
|
726
|
+
itemType: "agent",
|
|
727
|
+
renderItem: agent => {
|
|
728
|
+
const icon = formatStatusIcon("running", uiTheme, options.spinnerFrame);
|
|
729
|
+
const badge = formatBadge("agent", "accent", uiTheme);
|
|
730
|
+
const gist = agent.activity
|
|
731
|
+
? ` ${uiTheme.fg("toolOutput", truncateToWidth(replaceTabs(agent.activity), LABEL_MAX_WIDTH, Ellipsis.Unicode))}`
|
|
732
|
+
: "";
|
|
733
|
+
const parent = agent.parentId ? uiTheme.fg("dim", ` ← ${agent.parentId}`) : "";
|
|
734
|
+
const age = uiTheme.fg("dim", formatDuration(agent.ageMs));
|
|
735
|
+
return [`${icon} ${uiTheme.fg("muted", agent.id)} ${badge}${gist} ${age}${parent}`];
|
|
736
|
+
},
|
|
737
|
+
},
|
|
738
|
+
uiTheme,
|
|
739
|
+
);
|
|
740
|
+
|
|
741
|
+
const all = [header, ...itemLines, ...agentLines].map(l => truncateToWidth(l, width, Ellipsis.Unicode));
|
|
602
742
|
cached = { key, lines: all };
|
|
603
743
|
return all;
|
|
604
744
|
},
|
package/src/tools/write.ts
CHANGED
|
@@ -82,6 +82,30 @@ import { toolResult } from "./tool-result";
|
|
|
82
82
|
const LOOSE_HASHLINE_HEADER_RE = /^\s*\[[^#\r\n]+#[^ \t\r\n]*\]\s*$/;
|
|
83
83
|
const EXECUTABLE_NOTICE = "[Notice: Made executable via chmod +x]";
|
|
84
84
|
|
|
85
|
+
const BULK_DIRECTIVE_RE = /^#?(\d+)\s*[:=]\s*(@ours|@theirs|@base|@both)$/;
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Parse `conflict://*` per-id directive content: every non-empty line must be
|
|
89
|
+
* `<id>: @side` (also accepted: `#<id> = @side`). Returns `null` when the
|
|
90
|
+
* content is not directive-shaped (→ uniform bulk mode); throws on duplicate
|
|
91
|
+
* ids so a typo never silently drops a resolution.
|
|
92
|
+
*/
|
|
93
|
+
function parseBulkDirectives(content: string): Map<number, string> | null {
|
|
94
|
+
const map = new Map<number, string>();
|
|
95
|
+
for (const raw of content.split("\n")) {
|
|
96
|
+
const line = raw.trim();
|
|
97
|
+
if (line.length === 0) continue;
|
|
98
|
+
const match = line.match(BULK_DIRECTIVE_RE);
|
|
99
|
+
if (!match) return null;
|
|
100
|
+
const id = Number.parseInt(match[1], 10);
|
|
101
|
+
if (map.has(id)) {
|
|
102
|
+
throw new ToolError(`Bulk directive lists conflict #${id} twice — each id may appear once.`);
|
|
103
|
+
}
|
|
104
|
+
map.set(id, match[2]);
|
|
105
|
+
}
|
|
106
|
+
return map.size > 0 ? map : null;
|
|
107
|
+
}
|
|
108
|
+
|
|
85
109
|
const writeSchema = type({
|
|
86
110
|
path: type("string").describe("file path"),
|
|
87
111
|
content: type("string").describe("file content"),
|
|
@@ -591,7 +615,8 @@ export class WriteTool implements AgentTool<typeof writeSchema, WriteToolDetails
|
|
|
591
615
|
|
|
592
616
|
const expanded = expandContentTokens(replacementContent, entry);
|
|
593
617
|
const originalText = await Bun.file(absolutePath).text();
|
|
594
|
-
const
|
|
618
|
+
const splice = spliceConflict(originalText, entry, expanded);
|
|
619
|
+
const newContent = splice.text;
|
|
595
620
|
|
|
596
621
|
await writethroughNoop(absolutePath, newContent, signal);
|
|
597
622
|
invalidateFsScanAfterWrite(absolutePath);
|
|
@@ -626,6 +651,10 @@ export class WriteTool implements AgentTool<typeof writeSchema, WriteToolDetails
|
|
|
626
651
|
if (stripped) {
|
|
627
652
|
resultText += `\nNote: auto-stripped hashline display prefixes from content before writing.`;
|
|
628
653
|
}
|
|
654
|
+
const echoTrimmed = splice.trimmedLeading + splice.trimmedTrailing;
|
|
655
|
+
if (echoTrimmed > 0) {
|
|
656
|
+
resultText += `\nNote: dropped ${echoTrimmed} content line(s) that duplicated the code adjacent to the conflict region — writes replace only the marker block; surrounding lines stay in place.`;
|
|
657
|
+
}
|
|
629
658
|
|
|
630
659
|
return {
|
|
631
660
|
content: [{ type: "text", text: resultText }],
|
|
@@ -671,6 +700,7 @@ export class WriteTool implements AgentTool<typeof writeSchema, WriteToolDetails
|
|
|
671
700
|
replacementContent: string,
|
|
672
701
|
stripped: boolean,
|
|
673
702
|
signal: AbortSignal | undefined,
|
|
703
|
+
rawContent: string = replacementContent,
|
|
674
704
|
): Promise<AgentToolResult<WriteToolDetails>> {
|
|
675
705
|
const history = getConflictHistory(this.session);
|
|
676
706
|
const allEntries = history.entries();
|
|
@@ -680,8 +710,28 @@ export class WriteTool implements AgentTool<typeof writeSchema, WriteToolDetails
|
|
|
680
710
|
);
|
|
681
711
|
}
|
|
682
712
|
|
|
713
|
+
// Per-id directive mode: content made solely of `<id>: @side` lines
|
|
714
|
+
// resolves each listed conflict with that side in one call. Ideal for
|
|
715
|
+
// merge-hell files where dozens of pick-one blocks each need their own
|
|
716
|
+
// winner — one call instead of one write per conflict. Parsed from the
|
|
717
|
+
// PRE-strip content: hashline prefix stripping would otherwise eat the
|
|
718
|
+
// `<id>: ` heads as echoed line numbers.
|
|
719
|
+
const directives = parseBulkDirectives(rawContent) ?? parseBulkDirectives(replacementContent);
|
|
720
|
+
if (directives) {
|
|
721
|
+
const known = new Set(allEntries.map(entry => entry.id));
|
|
722
|
+
const unknown = [...directives.keys()].filter(id => !known.has(id));
|
|
723
|
+
if (unknown.length > 0) {
|
|
724
|
+
throw new ToolError(
|
|
725
|
+
`Bulk directive references unknown conflict id(s) ${unknown.map(id => `#${id}`).join(", ")}. Currently registered: ${allEntries.map(e => `#${e.id}`).join(", ")}.`,
|
|
726
|
+
);
|
|
727
|
+
}
|
|
728
|
+
}
|
|
729
|
+
const selectedEntries = directives ? allEntries.filter(entry => directives.has(entry.id)) : allEntries;
|
|
730
|
+
const contentFor = (entry: ConflictEntry): string =>
|
|
731
|
+
directives ? (directives.get(entry.id) as string) : replacementContent;
|
|
732
|
+
|
|
683
733
|
const byFile = new Map<string, ConflictEntry[]>();
|
|
684
|
-
for (const entry of
|
|
734
|
+
for (const entry of selectedEntries) {
|
|
685
735
|
const bucket = byFile.get(entry.absolutePath) ?? [];
|
|
686
736
|
bucket.push(entry);
|
|
687
737
|
byFile.set(entry.absolutePath, bucket);
|
|
@@ -690,6 +740,7 @@ export class WriteTool implements AgentTool<typeof writeSchema, WriteToolDetails
|
|
|
690
740
|
const succeededFiles: { displayPath: string; count: number; header?: string }[] = [];
|
|
691
741
|
const failedFiles: { displayPath: string; count: number; error: string }[] = [];
|
|
692
742
|
let totalResolvedIds = 0;
|
|
743
|
+
let totalEchoTrimmed = 0;
|
|
693
744
|
|
|
694
745
|
for (const [absolutePath, fileEntries] of byFile) {
|
|
695
746
|
const sample = fileEntries[0]!;
|
|
@@ -720,8 +771,10 @@ export class WriteTool implements AgentTool<typeof writeSchema, WriteToolDetails
|
|
|
720
771
|
}
|
|
721
772
|
for (const entry of fileEntries) {
|
|
722
773
|
try {
|
|
723
|
-
const expanded = expandContentTokens(
|
|
724
|
-
|
|
774
|
+
const expanded = expandContentTokens(contentFor(entry), entry);
|
|
775
|
+
const splice = spliceConflict(text, entry, expanded);
|
|
776
|
+
text = splice.text;
|
|
777
|
+
totalEchoTrimmed += splice.trimmedLeading + splice.trimmedTrailing;
|
|
725
778
|
resolvedEntries.push(entry);
|
|
726
779
|
} catch (error) {
|
|
727
780
|
// A locate-miss for a region an earlier entry already spliced
|
|
@@ -766,6 +819,17 @@ export class WriteTool implements AgentTool<typeof writeSchema, WriteToolDetails
|
|
|
766
819
|
summaryLines.push(` ${file.displayPath}: ${file.count} ${conflictWord(file.count)}`);
|
|
767
820
|
}
|
|
768
821
|
}
|
|
822
|
+
if (directives && selectedEntries.length < allEntries.length) {
|
|
823
|
+
const remaining = allEntries.filter(entry => !directives.has(entry.id)).map(entry => `#${entry.id}`);
|
|
824
|
+
summaryLines.push(
|
|
825
|
+
`Directive mode: ${remaining.length} unlisted ${conflictWord(remaining.length)} still registered (${remaining.join(", ")}).`,
|
|
826
|
+
);
|
|
827
|
+
}
|
|
828
|
+
if (totalEchoTrimmed > 0) {
|
|
829
|
+
summaryLines.push(
|
|
830
|
+
`Note: dropped ${totalEchoTrimmed} content line(s) that duplicated code adjacent to conflict regions — writes replace only the marker block; surrounding lines stay in place.`,
|
|
831
|
+
);
|
|
832
|
+
}
|
|
769
833
|
if (failedFiles.length > 0) {
|
|
770
834
|
summaryLines.push(
|
|
771
835
|
`Failed to resolve ${failedFiles.length} ${fileWord(failedFiles.length)} — registered entries left intact for retry:`,
|
|
@@ -781,7 +845,7 @@ export class WriteTool implements AgentTool<typeof writeSchema, WriteToolDetails
|
|
|
781
845
|
summaryLines.push("Snapshots:");
|
|
782
846
|
for (const header of headerLines) summaryLines.push(` ${header}`);
|
|
783
847
|
}
|
|
784
|
-
if (stripped) {
|
|
848
|
+
if (stripped && !directives) {
|
|
785
849
|
summaryLines.push("Note: auto-stripped hashline display prefixes from content before writing.");
|
|
786
850
|
}
|
|
787
851
|
const resultText = summaryLines.join("\n");
|
|
@@ -849,7 +913,7 @@ export class WriteTool implements AgentTool<typeof writeSchema, WriteToolDetails
|
|
|
849
913
|
emitWriteProgress(onUpdate, cleanContent, path);
|
|
850
914
|
const result =
|
|
851
915
|
conflictUri.id === "*"
|
|
852
|
-
? await this.#resolveAllConflicts(cleanContent, stripped, signal)
|
|
916
|
+
? await this.#resolveAllConflicts(cleanContent, stripped, signal, content)
|
|
853
917
|
: await this.#resolveSingleConflictById(conflictUri.id, cleanContent, stripped, signal);
|
|
854
918
|
if (conflictUri.recoveredPrefix !== undefined) {
|
|
855
919
|
appendNoteToResult(
|
package/src/vibe/runtime.ts
CHANGED
|
@@ -603,7 +603,7 @@ export class VibeSessionRegistry {
|
|
|
603
603
|
);
|
|
604
604
|
}
|
|
605
605
|
},
|
|
606
|
-
{ id: `${record.id}-t${turnIndex}`, ownerId: record.ownerId },
|
|
606
|
+
{ id: `${record.id}-t${turnIndex}`, agentId: record.id, ownerId: record.ownerId },
|
|
607
607
|
);
|
|
608
608
|
turn.jobId = jobId;
|
|
609
609
|
record.turn = turn;
|
|
@@ -1,22 +1,35 @@
|
|
|
1
1
|
import { HeaderGenerator } from "header-generator";
|
|
2
2
|
|
|
3
|
-
//
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
3
|
+
// Lazily instantiate the singleton header generator. Bun single-file binaries do not
|
|
4
|
+
// bundle header-generator's fs-loaded data_files, so construction may throw when the
|
|
5
|
+
// original build-time node_modules path is absent.
|
|
6
|
+
let generator: HeaderGenerator | undefined;
|
|
7
|
+
let generatorUnavailable = false;
|
|
8
|
+
|
|
9
|
+
function getHeaderGenerator(): HeaderGenerator | undefined {
|
|
10
|
+
if (generatorUnavailable) return undefined;
|
|
11
|
+
try {
|
|
12
|
+
generator ??= new HeaderGenerator({
|
|
13
|
+
browserListQuery: "last 3 versions",
|
|
14
|
+
devices: ["desktop"],
|
|
15
|
+
operatingSystems: ["windows", "macos", "linux"],
|
|
16
|
+
locales: ["en-US", "en"],
|
|
17
|
+
httpVersion: "2",
|
|
18
|
+
strict: false,
|
|
19
|
+
});
|
|
20
|
+
return generator;
|
|
21
|
+
} catch {
|
|
22
|
+
generatorUnavailable = true;
|
|
23
|
+
return undefined;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
14
26
|
|
|
15
27
|
// A fallback desktop Mac Chrome navigation fingerprint matching
|
|
16
28
|
// the previous static default setup for deterministic or non-randomized calls.
|
|
17
29
|
const CHROME_FALLBACK_HEADERS: Record<string, string> = {
|
|
18
30
|
Accept:
|
|
19
31
|
"text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7",
|
|
32
|
+
"Accept-Encoding": "gzip, deflate, br, zstd",
|
|
20
33
|
"Accept-Language": "en-US,en;q=0.9",
|
|
21
34
|
"Cache-Control": "max-age=0",
|
|
22
35
|
Priority: "u=0, i",
|
|
@@ -81,10 +94,14 @@ export function buildBrowserNavigationHeaders(options?: { randomized?: boolean }
|
|
|
81
94
|
return { ...CHROME_FALLBACK_HEADERS };
|
|
82
95
|
}
|
|
83
96
|
|
|
97
|
+
const generator = getHeaderGenerator();
|
|
98
|
+
if (!generator) {
|
|
99
|
+
return { ...CHROME_FALLBACK_HEADERS };
|
|
100
|
+
}
|
|
101
|
+
|
|
84
102
|
try {
|
|
85
103
|
// Generate realistic, consistent headers with the Bayesian generator
|
|
86
|
-
|
|
87
|
-
return canonicalizeHeaderNames(generated);
|
|
104
|
+
return canonicalizeHeaderNames(generator.getHeaders());
|
|
88
105
|
} catch {
|
|
89
106
|
// Gracefully recover to the robust default profile on unexpected generator errors
|
|
90
107
|
return { ...CHROME_FALLBACK_HEADERS };
|
|
@@ -1,37 +0,0 @@
|
|
|
1
|
-
import type { Model } from "@oh-my-pi/pi-ai";
|
|
2
|
-
import { Container, Input, type SgrMouseEvent, type TUI } from "@oh-my-pi/pi-tui";
|
|
3
|
-
import type { ModelRegistry } from "../../config/model-registry.js";
|
|
4
|
-
import type { Settings } from "../../config/settings.js";
|
|
5
|
-
import { type ConfiguredThinkingLevel } from "../../thinking.js";
|
|
6
|
-
interface ScopedModelItem {
|
|
7
|
-
model: Model;
|
|
8
|
-
thinkingLevel?: string;
|
|
9
|
-
}
|
|
10
|
-
type ModelSelectorAction = "modelRole" | "retryFallback";
|
|
11
|
-
type RoleSelectCallback = (model: Model, role: string | null, thinkingLevel?: ConfiguredThinkingLevel, selector?: string, action?: ModelSelectorAction) => void;
|
|
12
|
-
/**
|
|
13
|
-
* Component that renders a model selector with provider tabs and context menu.
|
|
14
|
-
* - Tab/Arrow Left/Right: Switch between provider tabs
|
|
15
|
-
* - Arrow Up/Down: Navigate model list
|
|
16
|
-
* - Enter: Open context menu to select action
|
|
17
|
-
* - Escape: Close menu or selector
|
|
18
|
-
*/
|
|
19
|
-
export declare class ModelSelectorComponent extends Container {
|
|
20
|
-
#private;
|
|
21
|
-
constructor(tui: TUI, _currentModel: Model | undefined, settings: Settings, modelRegistry: ModelRegistry, scopedModels: ReadonlyArray<ScopedModelItem>, onSelect: RoleSelectCallback, onCancel: () => void, options?: {
|
|
22
|
-
temporaryOnly?: boolean;
|
|
23
|
-
directSelect?: boolean;
|
|
24
|
-
pickerHint?: string;
|
|
25
|
-
initialSearchInput?: string;
|
|
26
|
-
currentContextTokens?: number;
|
|
27
|
-
});
|
|
28
|
-
/**
|
|
29
|
-
* Concatenate children like Container.render, recording where the model list
|
|
30
|
-
* lands so routed mouse events can be hit-tested against it.
|
|
31
|
-
*/
|
|
32
|
-
render(width: number): readonly string[];
|
|
33
|
-
routeMouse(event: SgrMouseEvent, line: number, _col: number): void;
|
|
34
|
-
handleInput(keyData: string): void;
|
|
35
|
-
getSearchInput(): Input;
|
|
36
|
-
}
|
|
37
|
-
export {};
|