jeopi-cli 16.4.3 → 16.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/dist/cli.js +3094 -3091
- package/dist/types/advisor/advise-tool.d.ts +5 -1
- package/dist/types/advisor/runtime.d.ts +15 -0
- package/dist/types/config/model-registry.d.ts +3 -3
- package/dist/types/config/models-config.d.ts +1 -1
- package/dist/types/config/settings-schema.d.ts +0 -10
- package/dist/types/modes/rpc/host-tools.d.ts +2 -0
- package/dist/types/modes/rpc/rpc-mode.d.ts +27 -7
- package/dist/types/modes/utils/transcript-render-helpers.d.ts +9 -0
- package/dist/types/session/agent-session.d.ts +1 -0
- package/package.json +12 -12
- package/src/advisor/advise-tool.ts +6 -1
- package/src/advisor/runtime.ts +127 -0
- package/src/autolearn/controller.ts +1 -1
- package/src/config/config-file.ts +30 -12
- package/src/config/model-registry.ts +7 -7
- package/src/config/models-config.ts +1 -1
- package/src/config/settings-schema.ts +0 -12
- package/src/extensibility/plugins/legacy-pi-bundled-keys.ts +9 -1
- package/src/extensibility/plugins/legacy-pi-bundled-registry.ts +30 -3
- package/src/internal-urls/docs-index.generated.txt +1 -1
- package/src/modes/components/chat-transcript-builder.ts +31 -11
- package/src/modes/controllers/event-controller.ts +41 -12
- package/src/modes/rpc/host-tools.ts +13 -0
- package/src/modes/rpc/rpc-mode.ts +130 -56
- package/src/modes/utils/transcript-render-helpers.ts +44 -0
- package/src/modes/utils/ui-helpers.ts +16 -1
- package/src/session/agent-session.ts +95 -11
- package/src/task/render.ts +51 -44
- package/src/tools/bash.ts +0 -11
- package/src/tools/path-utils.ts +6 -1
- package/src/utils/title-generator.ts +5 -1
- package/dist/types/tools/bash-command-fixup.d.ts +0 -11
- package/src/tools/bash-command-fixup.ts +0 -37
|
@@ -144,13 +144,16 @@ import {
|
|
|
144
144
|
AdvisorEmissionGuard,
|
|
145
145
|
type AdvisorMessageDetails,
|
|
146
146
|
type AdvisorNote,
|
|
147
|
+
AdvisorOutputQuarantinedError,
|
|
147
148
|
AdvisorRuntime,
|
|
148
149
|
type AdvisorSeverity,
|
|
149
150
|
AdvisorTranscriptRecorder,
|
|
150
151
|
advisorTranscriptFilename,
|
|
152
|
+
buildAdvisorQuarantineSourceText,
|
|
151
153
|
formatAdvisorBatchContent,
|
|
152
154
|
isAdvisorInterruptImmuneTurnActive,
|
|
153
155
|
isInterruptingSeverity,
|
|
156
|
+
quarantineAdvisorUnsafeOutput,
|
|
154
157
|
resolveAdvisorDeliveryChannel,
|
|
155
158
|
slugifyAdvisorName,
|
|
156
159
|
} from "../advisor";
|
|
@@ -1445,6 +1448,20 @@ function isDisplayableQueuedMessage(message: AgentMessage): boolean {
|
|
|
1445
1448
|
function isAdvisorCard(message: AgentMessage): message is CustomMessage {
|
|
1446
1449
|
return message.role === "custom" && message.customType === "advisor";
|
|
1447
1450
|
}
|
|
1451
|
+
function isTerminalTextAssistantAnswer(message: AgentMessage | undefined): message is AssistantMessage {
|
|
1452
|
+
if (message?.role !== "assistant" || message.stopReason !== "stop") return false;
|
|
1453
|
+
let hasText = false;
|
|
1454
|
+
for (const part of message.content) {
|
|
1455
|
+
if (part.type === "toolCall") return false;
|
|
1456
|
+
if (part.type === "text") {
|
|
1457
|
+
if (part.text.trim().length > 0) hasText = true;
|
|
1458
|
+
continue;
|
|
1459
|
+
}
|
|
1460
|
+
if (part.type === "thinking") continue;
|
|
1461
|
+
return false;
|
|
1462
|
+
}
|
|
1463
|
+
return hasText;
|
|
1464
|
+
}
|
|
1448
1465
|
|
|
1449
1466
|
/**
|
|
1450
1467
|
* A queued message the user can restore to the editor / pull back as a draft.
|
|
@@ -1887,6 +1904,7 @@ export class AgentSession {
|
|
|
1887
1904
|
// Cursor exec, TUI listeners) is held back. Without this, a client that resumes
|
|
1888
1905
|
// on `agent_end` can fire its next `prompt` before #promptWithMessage's finally
|
|
1889
1906
|
#emptyStopRetryCount = 0;
|
|
1907
|
+
#acceptTerminalEmptyStopForPrompt = false;
|
|
1890
1908
|
#unexpectedStopRetryCount = 0;
|
|
1891
1909
|
#promptGeneration = 0;
|
|
1892
1910
|
#pendingAgentEndEmit: AgentSessionEvent | undefined;
|
|
@@ -2475,6 +2493,14 @@ export class AgentSession {
|
|
|
2475
2493
|
|
|
2476
2494
|
const names = config.tools === undefined ? ADVISOR_DEFAULT_TOOL_NAMES : new Set(config.tools);
|
|
2477
2495
|
const tools = (this.#advisorTools ?? []).filter(t => names.has(t.name));
|
|
2496
|
+
const availableAdvisorToolNames = new Set<string>();
|
|
2497
|
+
availableAdvisorToolNames.add(adviseTool.name);
|
|
2498
|
+
for (const tool of tools) {
|
|
2499
|
+
availableAdvisorToolNames.add(tool.name);
|
|
2500
|
+
if (tool.customWireName !== undefined) availableAdvisorToolNames.add(tool.customWireName);
|
|
2501
|
+
}
|
|
2502
|
+
let quarantinedAdvisorOutput: string | undefined;
|
|
2503
|
+
let currentAdvisorInput = "";
|
|
2478
2504
|
|
|
2479
2505
|
const advisorSessionId = this.#advisorSessionId(slug);
|
|
2480
2506
|
const appendOnlyContext = new AppendOnlyContextManager();
|
|
@@ -2519,6 +2545,13 @@ export class AgentSession {
|
|
|
2519
2545
|
streamFn: this.#advisorStreamFn,
|
|
2520
2546
|
onPayload: this.#onPayload,
|
|
2521
2547
|
onResponse: this.#onResponse,
|
|
2548
|
+
transformAssistantMessage: message => {
|
|
2549
|
+
quarantinedAdvisorOutput = quarantineAdvisorUnsafeOutput(
|
|
2550
|
+
message,
|
|
2551
|
+
availableAdvisorToolNames,
|
|
2552
|
+
buildAdvisorQuarantineSourceText(currentAdvisorInput, advisorAgent.state.messages),
|
|
2553
|
+
);
|
|
2554
|
+
},
|
|
2522
2555
|
onSseEvent: this.#onSseEvent,
|
|
2523
2556
|
transformProviderContext: this.#transformProviderContext,
|
|
2524
2557
|
intentTracing: false,
|
|
@@ -2529,7 +2562,19 @@ export class AgentSession {
|
|
|
2529
2562
|
advisorAgent.setDisableReasoning(shouldDisableReasoning(advisorThinkingLevel));
|
|
2530
2563
|
|
|
2531
2564
|
const advisorAgentFacade: AdvisorAgent = {
|
|
2532
|
-
prompt: input =>
|
|
2565
|
+
prompt: async input => {
|
|
2566
|
+
let quarantined: string | undefined;
|
|
2567
|
+
try {
|
|
2568
|
+
quarantinedAdvisorOutput = undefined;
|
|
2569
|
+
currentAdvisorInput = input;
|
|
2570
|
+
await advisorAgent.prompt(input);
|
|
2571
|
+
quarantined = quarantinedAdvisorOutput;
|
|
2572
|
+
} finally {
|
|
2573
|
+
quarantinedAdvisorOutput = undefined;
|
|
2574
|
+
currentAdvisorInput = "";
|
|
2575
|
+
}
|
|
2576
|
+
if (quarantined) throw new AdvisorOutputQuarantinedError(quarantined);
|
|
2577
|
+
},
|
|
2533
2578
|
abort: reason => advisorAgent.abort(reason),
|
|
2534
2579
|
reset: () => {
|
|
2535
2580
|
advisorAgent.reset();
|
|
@@ -2635,15 +2680,23 @@ export class AgentSession {
|
|
|
2635
2680
|
return slug ? `${this.sessionId}-advisor-${slug}` : `${this.sessionId}-advisor`;
|
|
2636
2681
|
}
|
|
2637
2682
|
|
|
2683
|
+
#hasTerminalTextAnswerWithoutQueuedWork(): boolean {
|
|
2684
|
+
if (this.agent.hasQueuedMessages() || this.#pendingNextTurnMessages.length > 0) return false;
|
|
2685
|
+
return isTerminalTextAssistantAnswer(this.agent.state.messages.at(-1));
|
|
2686
|
+
}
|
|
2687
|
+
|
|
2638
2688
|
/**
|
|
2639
2689
|
* Route one accepted advice note from `advisor` to the primary. Concern and
|
|
2640
2690
|
* blocker interrupt the running agent through the steering channel; once the
|
|
2641
|
-
* loop has yielded, `triggerTurn` resumes it.
|
|
2642
|
-
*
|
|
2643
|
-
* card
|
|
2644
|
-
*
|
|
2645
|
-
*
|
|
2646
|
-
*
|
|
2691
|
+
* loop has yielded, `triggerTurn` resumes it. If the loop already ended with a
|
|
2692
|
+
* terminal text answer and no queued work remains, the note is preserved as an
|
|
2693
|
+
* advisor card instead of waking a duplicate completion turn. After a deliberate
|
|
2694
|
+
* user interrupt auto-resume is suppressed while idle/unwinding (the note
|
|
2695
|
+
* becomes a preserved card re-entering on resume); a live-streaming turn is
|
|
2696
|
+
* steered in directly. A plain nit always rides the non-interrupting YieldQueue
|
|
2697
|
+
* aside. Suppression by the per-advisor emission guard drops the note silently —
|
|
2698
|
+
* the model still saw `Recorded.`, so it isn't tempted to rephrase the same note
|
|
2699
|
+
* past the dedupe.
|
|
2647
2700
|
*/
|
|
2648
2701
|
#routeAdvice(advisor: ActiveAdvisor, note: string, severity?: AdvisorSeverity): void {
|
|
2649
2702
|
if (!advisor.emissionGuard.accept(note)) {
|
|
@@ -2663,6 +2716,7 @@ export class AgentSession {
|
|
|
2663
2716
|
streaming: this.agent.state.isStreaming,
|
|
2664
2717
|
aborting: this.#abortInProgress,
|
|
2665
2718
|
interruptImmuneTurnActive: interrupting && this.#isAdvisorInterruptImmuneTurnActive(),
|
|
2719
|
+
terminalAnswerNoQueuedWork: this.#hasTerminalTextAnswerWithoutQueuedWork(),
|
|
2666
2720
|
});
|
|
2667
2721
|
if (channel === "aside") {
|
|
2668
2722
|
this.yieldQueue.enqueue("advisor", { note, severity, advisor: source });
|
|
@@ -7571,6 +7625,7 @@ export class AgentSession {
|
|
|
7571
7625
|
options?: Pick<PromptOptions, "toolChoice" | "images" | "skipCompactionCheck"> & {
|
|
7572
7626
|
prependMessages?: AgentMessage[];
|
|
7573
7627
|
skipPostPromptRecoveryWait?: boolean;
|
|
7628
|
+
acceptTerminalEmptyStop?: boolean;
|
|
7574
7629
|
},
|
|
7575
7630
|
): Promise<void> {
|
|
7576
7631
|
this.#beginInFlight();
|
|
@@ -7588,6 +7643,7 @@ export class AgentSession {
|
|
|
7588
7643
|
this.#midRunNudgeCount = 0;
|
|
7589
7644
|
this.#emptyStopRetryCount = 0;
|
|
7590
7645
|
this.#unexpectedStopRetryCount = 0;
|
|
7646
|
+
this.#acceptTerminalEmptyStopForPrompt = options?.acceptTerminalEmptyStop === true;
|
|
7591
7647
|
// A new prompt cycle starts: drop any sticky yield-termination from the
|
|
7592
7648
|
// previous run so empty-stop / unexpected-stop / compaction maintenance
|
|
7593
7649
|
// can evaluate this turn normally.
|
|
@@ -8092,12 +8148,25 @@ export class AgentSession {
|
|
|
8092
8148
|
}
|
|
8093
8149
|
}
|
|
8094
8150
|
|
|
8095
|
-
async #promptAgentInitiatedMessage(
|
|
8151
|
+
async #promptAgentInitiatedMessage(
|
|
8152
|
+
message: CustomMessage,
|
|
8153
|
+
options?: { acceptTerminalEmptyStop?: boolean },
|
|
8154
|
+
): Promise<void> {
|
|
8096
8155
|
this.#beginInFlight();
|
|
8156
|
+
const acceptTerminalEmptyStop = options?.acceptTerminalEmptyStop === true;
|
|
8097
8157
|
try {
|
|
8158
|
+
if (acceptTerminalEmptyStop) {
|
|
8159
|
+
this.#emptyStopRetryCount = 0;
|
|
8160
|
+
this.#unexpectedStopRetryCount = 0;
|
|
8161
|
+
this.#yieldTerminationPending = false;
|
|
8162
|
+
this.#acceptTerminalEmptyStopForPrompt = true;
|
|
8163
|
+
} else {
|
|
8164
|
+
this.#acceptTerminalEmptyStopForPrompt = false;
|
|
8165
|
+
}
|
|
8098
8166
|
await this.agent.prompt(message);
|
|
8099
8167
|
await this.#waitForPostPromptRecovery();
|
|
8100
8168
|
} finally {
|
|
8169
|
+
this.#acceptTerminalEmptyStopForPrompt = false;
|
|
8101
8170
|
this.#endInFlight();
|
|
8102
8171
|
}
|
|
8103
8172
|
}
|
|
@@ -8152,7 +8221,12 @@ export class AgentSession {
|
|
|
8152
8221
|
*/
|
|
8153
8222
|
async sendCustomMessage<T = unknown>(
|
|
8154
8223
|
message: Pick<CustomMessage<T>, "customType" | "content" | "display" | "details" | "attribution">,
|
|
8155
|
-
options?: {
|
|
8224
|
+
options?: {
|
|
8225
|
+
triggerTurn?: boolean;
|
|
8226
|
+
deliverAs?: "steer" | "followUp" | "nextTurn";
|
|
8227
|
+
queueChipText?: string;
|
|
8228
|
+
acceptTerminalEmptyStop?: boolean;
|
|
8229
|
+
},
|
|
8156
8230
|
): Promise<boolean> {
|
|
8157
8231
|
const details =
|
|
8158
8232
|
options?.queueChipText && options.deliverAs !== "nextTurn"
|
|
@@ -8195,7 +8269,9 @@ export class AgentSession {
|
|
|
8195
8269
|
this.#queueHiddenNextTurnMessage(normalizedAppMessage, false);
|
|
8196
8270
|
return false;
|
|
8197
8271
|
}
|
|
8198
|
-
await this.#promptAgentInitiatedMessage(normalizedAppMessage
|
|
8272
|
+
await this.#promptAgentInitiatedMessage(normalizedAppMessage, {
|
|
8273
|
+
acceptTerminalEmptyStop: options.acceptTerminalEmptyStop === true,
|
|
8274
|
+
});
|
|
8199
8275
|
return true;
|
|
8200
8276
|
}
|
|
8201
8277
|
this.agent.appendMessage(normalizedAppMessage);
|
|
@@ -8214,7 +8290,9 @@ export class AgentSession {
|
|
|
8214
8290
|
this.#queueHiddenNextTurnMessage(normalizedAppMessage, false);
|
|
8215
8291
|
return false;
|
|
8216
8292
|
}
|
|
8217
|
-
await this.#promptAgentInitiatedMessage(normalizedAppMessage
|
|
8293
|
+
await this.#promptAgentInitiatedMessage(normalizedAppMessage, {
|
|
8294
|
+
acceptTerminalEmptyStop: options.acceptTerminalEmptyStop === true,
|
|
8295
|
+
});
|
|
8218
8296
|
return true;
|
|
8219
8297
|
}
|
|
8220
8298
|
|
|
@@ -10482,6 +10560,12 @@ export class AgentSession {
|
|
|
10482
10560
|
return false;
|
|
10483
10561
|
}
|
|
10484
10562
|
|
|
10563
|
+
if (this.#acceptTerminalEmptyStopForPrompt && assistantMessage.stopReason === "stop") {
|
|
10564
|
+
this.#acceptTerminalEmptyStopForPrompt = false;
|
|
10565
|
+
this.#emptyStopRetryCount = 0;
|
|
10566
|
+
return false;
|
|
10567
|
+
}
|
|
10568
|
+
|
|
10485
10569
|
this.#emptyStopRetryCount++;
|
|
10486
10570
|
if (this.#emptyStopRetryCount > EMPTY_STOP_MAX_RETRIES) {
|
|
10487
10571
|
const attempts = this.#emptyStopRetryCount - 1;
|
package/src/task/render.ts
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
import path from "node:path";
|
|
8
8
|
import type { Component } from "jeopi-tui";
|
|
9
9
|
import { Container, Markdown, Text } from "jeopi-tui";
|
|
10
|
-
import { formatNumber } from "jeopi-utils";
|
|
10
|
+
import { formatNumber, sanitizeText } from "jeopi-utils";
|
|
11
11
|
import { settings } from "../config/settings";
|
|
12
12
|
import type { RenderResultOptions } from "../extensibility/custom-tools/types";
|
|
13
13
|
import { formatContextUsage } from "../modes/components/status-line/context-thresholds";
|
|
@@ -249,11 +249,11 @@ function getRenderYieldLabels(type: RenderYieldItem["type"]): string[] {
|
|
|
249
249
|
function formatYieldPreview(item: RenderYieldItem): string {
|
|
250
250
|
if (item.useLastTurn === true && item.data === undefined) return "last assistant turn";
|
|
251
251
|
if (item.data === undefined) return "last assistant turn";
|
|
252
|
-
if (typeof item.data === "string") return previewLine(replaceTabs(item.data), 70);
|
|
252
|
+
if (typeof item.data === "string") return previewLine(replaceTabs(sanitizeText(item.data)), 70);
|
|
253
253
|
try {
|
|
254
|
-
return previewLine(replaceTabs(JSON.stringify(item.data) ?? "null"), 70);
|
|
254
|
+
return previewLine(replaceTabs(sanitizeText(JSON.stringify(item.data) ?? "null")), 70);
|
|
255
255
|
} catch {
|
|
256
|
-
return previewLine(replaceTabs(String(item.data)), 70);
|
|
256
|
+
return previewLine(replaceTabs(sanitizeText(String(item.data))), 70);
|
|
257
257
|
}
|
|
258
258
|
}
|
|
259
259
|
|
|
@@ -281,7 +281,7 @@ function renderTypedYieldSections(value: unknown, continuePrefix: string, expand
|
|
|
281
281
|
function formatJsonScalar(value: unknown, _theme: Theme): string {
|
|
282
282
|
if (value === null) return "null";
|
|
283
283
|
if (typeof value === "string") {
|
|
284
|
-
const trimmed = truncateToWidth(value, 70);
|
|
284
|
+
const trimmed = truncateToWidth(sanitizeText(value), 70);
|
|
285
285
|
return `"${trimmed}"`;
|
|
286
286
|
}
|
|
287
287
|
if (typeof value === "number" || typeof value === "boolean") return String(value);
|
|
@@ -291,8 +291,9 @@ function formatJsonScalar(value: unknown, _theme: Theme): string {
|
|
|
291
291
|
export function formatTaskId(id: string): string {
|
|
292
292
|
// Ids are name-based (e.g. "Anna", "Anna-2"); a "." separates nesting levels
|
|
293
293
|
// (e.g. "Anna.Bob"). Render the hierarchy with a ">" breadcrumb.
|
|
294
|
-
const
|
|
295
|
-
|
|
294
|
+
const sanitizedId = sanitizeText(id);
|
|
295
|
+
const segments = sanitizedId.split(".");
|
|
296
|
+
return segments.length < 2 ? sanitizedId : segments.join(">");
|
|
296
297
|
}
|
|
297
298
|
|
|
298
299
|
const MISSING_YIELD_WARNING_PREFIX = "SYSTEM WARNING: Subagent exited without calling yield tool";
|
|
@@ -347,13 +348,13 @@ function renderJsonTreeLines(
|
|
|
347
348
|
const scalar = formatJsonScalar(val, theme);
|
|
348
349
|
|
|
349
350
|
if (scalar) {
|
|
350
|
-
const label = key ? theme.fg("muted", key) : theme.fg("muted", "value");
|
|
351
|
+
const label = key ? theme.fg("muted", sanitizeText(key)) : theme.fg("muted", "value");
|
|
351
352
|
pushLine(`${prefix}${iconScalar} ${label}: ${theme.fg("dim", scalar)}`);
|
|
352
353
|
return;
|
|
353
354
|
}
|
|
354
355
|
|
|
355
356
|
if (Array.isArray(val)) {
|
|
356
|
-
const header = key ? theme.fg("muted", key) : theme.fg("muted", "array");
|
|
357
|
+
const header = key ? theme.fg("muted", sanitizeText(key)) : theme.fg("muted", "array");
|
|
357
358
|
pushLine(`${prefix}${iconArray} ${header}`);
|
|
358
359
|
if (val.length === 0) {
|
|
359
360
|
pushLine(
|
|
@@ -385,7 +386,7 @@ function renderJsonTreeLines(
|
|
|
385
386
|
}
|
|
386
387
|
|
|
387
388
|
if (val && typeof val === "object") {
|
|
388
|
-
const header = key ? theme.fg("muted", key) : theme.fg("muted", "object");
|
|
389
|
+
const header = key ? theme.fg("muted", sanitizeText(key)) : theme.fg("muted", "object");
|
|
389
390
|
pushLine(`${prefix}${iconObject} ${header}`);
|
|
390
391
|
const entries = Object.entries(val as Record<string, unknown>);
|
|
391
392
|
if (entries.length === 0) {
|
|
@@ -418,8 +419,8 @@ function renderJsonTreeLines(
|
|
|
418
419
|
return;
|
|
419
420
|
}
|
|
420
421
|
|
|
421
|
-
const label = key ? theme.fg("muted", key) : theme.fg("muted", "value");
|
|
422
|
-
pushLine(`${prefix}${iconScalar} ${label}: ${theme.fg("dim", String(val))}`);
|
|
422
|
+
const label = key ? theme.fg("muted", sanitizeText(key)) : theme.fg("muted", "value");
|
|
423
|
+
pushLine(`${prefix}${iconScalar} ${label}: ${theme.fg("dim", sanitizeText(String(val)))}`);
|
|
423
424
|
};
|
|
424
425
|
|
|
425
426
|
const renderRoot = (val: unknown) => {
|
|
@@ -466,7 +467,7 @@ function stripRecentOutputNoticeLine(text: string): string {
|
|
|
466
467
|
}
|
|
467
468
|
|
|
468
469
|
function sanitizeRecentOutput(output: string): string {
|
|
469
|
-
let text = output.trimEnd();
|
|
470
|
+
let text = sanitizeText(output).trimEnd();
|
|
470
471
|
while (text) {
|
|
471
472
|
const withoutArtifactNotice = stripRawOutputArtifactNotice(text).text;
|
|
472
473
|
if (withoutArtifactNotice !== text) {
|
|
@@ -498,7 +499,8 @@ function renderOutputSection(
|
|
|
498
499
|
warning?: string,
|
|
499
500
|
): string[] {
|
|
500
501
|
const lines: string[] = [];
|
|
501
|
-
const
|
|
502
|
+
const sanitizedOutput = sanitizeText(output);
|
|
503
|
+
const trimmedOutput = sanitizedOutput.trimEnd();
|
|
502
504
|
if (!trimmedOutput && !warning) return lines;
|
|
503
505
|
|
|
504
506
|
if (warning) {
|
|
@@ -506,7 +508,7 @@ function renderOutputSection(
|
|
|
506
508
|
lines.push(
|
|
507
509
|
`${continuePrefix} ${theme.fg("warning", theme.status.warning)} ${theme.fg(
|
|
508
510
|
"dim",
|
|
509
|
-
truncateToWidth(warning, 80),
|
|
511
|
+
truncateToWidth(sanitizeText(warning), 80),
|
|
510
512
|
)}`,
|
|
511
513
|
);
|
|
512
514
|
|
|
@@ -537,8 +539,7 @@ function renderOutputSection(
|
|
|
537
539
|
// Fall back to raw output
|
|
538
540
|
}
|
|
539
541
|
}
|
|
540
|
-
|
|
541
|
-
const outputLines = output.trimEnd().split("\n");
|
|
542
|
+
const outputLines = trimmedOutput.split("\n");
|
|
542
543
|
const previewCount = expanded ? maxExpanded : maxCollapsed;
|
|
543
544
|
for (const line of outputLines.slice(0, previewCount)) {
|
|
544
545
|
lines.push(`${continuePrefix} ${theme.fg("dim", truncateToWidth(replaceTabs(line), 70))}`);
|
|
@@ -581,8 +582,7 @@ function renderOutputSection(
|
|
|
581
582
|
}
|
|
582
583
|
|
|
583
584
|
lines.push(`${continuePrefix}${theme.fg("dim", "Output")}`);
|
|
584
|
-
|
|
585
|
-
const outputLines = output.trimEnd().split("\n");
|
|
585
|
+
const outputLines = trimmedOutput.split("\n");
|
|
586
586
|
const previewCount = expanded ? maxExpanded : maxCollapsed;
|
|
587
587
|
for (const line of outputLines.slice(0, previewCount)) {
|
|
588
588
|
lines.push(`${continuePrefix} ${theme.fg("dim", truncateToWidth(replaceTabs(line), 70))}`);
|
|
@@ -603,7 +603,7 @@ function renderTaskSection(
|
|
|
603
603
|
maxExpanded = 20,
|
|
604
604
|
): string[] {
|
|
605
605
|
const lines: string[] = [];
|
|
606
|
-
const trimmed = task.trim();
|
|
606
|
+
const trimmed = sanitizeText(task).trim();
|
|
607
607
|
if (!expanded || !trimmed) return lines;
|
|
608
608
|
|
|
609
609
|
lines.push(`${continuePrefix}${theme.fg("dim", "Task")}`);
|
|
@@ -624,10 +624,11 @@ function formatScalarInline(value: unknown, maxLen: number, _theme: Theme): stri
|
|
|
624
624
|
if (typeof value === "boolean") return String(value);
|
|
625
625
|
if (typeof value === "number") return String(value);
|
|
626
626
|
if (typeof value === "string") {
|
|
627
|
-
const
|
|
628
|
-
|
|
627
|
+
const sanitizedValue = sanitizeText(value);
|
|
628
|
+
const firstLine = sanitizedValue.split("\n")[0].trim();
|
|
629
|
+
if (firstLine.length === 0) return `"" (${sanitizedValue.split("\n").length} lines)`;
|
|
629
630
|
const preview = truncateToWidth(firstLine, maxLen);
|
|
630
|
-
if (
|
|
631
|
+
if (sanitizedValue.includes("\n")) return `"${preview}…" (${sanitizedValue.split("\n").length} lines)`;
|
|
631
632
|
return `"${preview}"`;
|
|
632
633
|
}
|
|
633
634
|
if (Array.isArray(value)) return `[${value.length} items]`;
|
|
@@ -635,7 +636,7 @@ function formatScalarInline(value: unknown, maxLen: number, _theme: Theme): stri
|
|
|
635
636
|
const keys = Object.keys(value);
|
|
636
637
|
return `{${keys.length} keys}`;
|
|
637
638
|
}
|
|
638
|
-
return String(value);
|
|
639
|
+
return sanitizeText(String(value));
|
|
639
640
|
}
|
|
640
641
|
|
|
641
642
|
function formatOutputInline(data: unknown, theme: Theme, maxWidth = 80): string {
|
|
@@ -662,7 +663,7 @@ function formatOutputInline(data: unknown, theme: Theme, maxWidth = 80): string
|
|
|
662
663
|
|
|
663
664
|
for (const [key, value] of entries) {
|
|
664
665
|
const valueStr = formatScalarInline(value, 24, theme);
|
|
665
|
-
const pairStr = `${key}=${valueStr}`;
|
|
666
|
+
const pairStr = `${sanitizeText(key)}=${valueStr}`;
|
|
666
667
|
const addLen = pairs.length > 0 ? pairStr.length + 2 : pairStr.length; // +2 for ", "
|
|
667
668
|
|
|
668
669
|
if (totalLen + addLen > maxWidth && pairs.length > 0) {
|
|
@@ -760,7 +761,9 @@ function createAssignmentSectionRenderer(
|
|
|
760
761
|
// `renderResult` receives the raw tool args (unlike `renderCall`, which is
|
|
761
762
|
// fed through `repairTaskParams`), so undo any per-field double-encoding
|
|
762
763
|
// here too. The repair is idempotent on already-clean text.
|
|
763
|
-
const assignment =
|
|
764
|
+
const assignment = sanitizeText(
|
|
765
|
+
repairDoubleEncodedJsonString(typeof args?.assignment === "string" ? args.assignment : ""),
|
|
766
|
+
).trim();
|
|
764
767
|
if (!assignment) return undefined;
|
|
765
768
|
return createMarkdownSectionRenderer(assignment, theme);
|
|
766
769
|
}
|
|
@@ -774,7 +777,9 @@ function createContextSectionRenderer(
|
|
|
774
777
|
args: Partial<TaskParams> | undefined,
|
|
775
778
|
theme: Theme,
|
|
776
779
|
): AssignmentSectionRenderer | undefined {
|
|
777
|
-
const context =
|
|
780
|
+
const context = sanitizeText(
|
|
781
|
+
repairDoubleEncodedJsonString(typeof args?.context === "string" ? args.context : ""),
|
|
782
|
+
).trim();
|
|
778
783
|
if (!context) return undefined;
|
|
779
784
|
return createMarkdownSectionRenderer(context, theme);
|
|
780
785
|
}
|
|
@@ -858,7 +863,7 @@ function renderAgentProgress(
|
|
|
858
863
|
: "accent";
|
|
859
864
|
|
|
860
865
|
// Main status line: id: description [status] · stats · ⟨agent⟩
|
|
861
|
-
const trimmedDescription = progress.description
|
|
866
|
+
const trimmedDescription = progress.description ? sanitizeText(progress.description).trim() : undefined;
|
|
862
867
|
const description = trimmedDescription ? previewLine(trimmedDescription, 64) : undefined;
|
|
863
868
|
const displayId = formatTaskId(progress.id);
|
|
864
869
|
const titlePart = description ? `${theme.bold(displayId)}: ${description}` : displayId;
|
|
@@ -900,7 +905,7 @@ function renderAgentProgress(
|
|
|
900
905
|
const showBadge = settings.get("task.showResolvedModelBadge");
|
|
901
906
|
if (progress.status === "running") {
|
|
902
907
|
if (!description) {
|
|
903
|
-
const taskPreview = previewLine(progress.assignment ?? progress.task, 40);
|
|
908
|
+
const taskPreview = previewLine(sanitizeText(progress.assignment ?? progress.task), 40);
|
|
904
909
|
statusLine += ` ${theme.fg("muted", taskPreview)}`;
|
|
905
910
|
}
|
|
906
911
|
statusLine = appendAgentStats(statusLine, { ...progress, showResolvedModelBadge: showBadge }, theme);
|
|
@@ -915,10 +920,10 @@ function renderAgentProgress(
|
|
|
915
920
|
// Current tool (if running) or most recent completed tool
|
|
916
921
|
if (progress.status === "running") {
|
|
917
922
|
if (progress.currentTool) {
|
|
918
|
-
let toolLine = `${continuePrefix}${theme.tree.hook} ${theme.fg("muted", progress.currentTool)}`;
|
|
923
|
+
let toolLine = `${continuePrefix}${theme.tree.hook} ${theme.fg("muted", sanitizeText(progress.currentTool))}`;
|
|
919
924
|
const toolDetail = progress.lastIntent ?? progress.currentToolArgs;
|
|
920
925
|
if (toolDetail) {
|
|
921
|
-
toolLine += `: ${theme.fg("dim", previewLine(toolDetail, 40))}`;
|
|
926
|
+
toolLine += `: ${theme.fg("dim", previewLine(sanitizeText(toolDetail), 40))}`;
|
|
922
927
|
}
|
|
923
928
|
if (progress.currentToolStartMs) {
|
|
924
929
|
const elapsed = Date.now() - progress.currentToolStartMs;
|
|
@@ -930,10 +935,10 @@ function renderAgentProgress(
|
|
|
930
935
|
} else if (progress.recentTools.length > 0) {
|
|
931
936
|
// Show most recent completed tool when idle between tools
|
|
932
937
|
const recent = progress.recentTools[0];
|
|
933
|
-
let toolLine = `${continuePrefix}${theme.tree.hook} ${theme.fg("dim", recent.tool)}`;
|
|
938
|
+
let toolLine = `${continuePrefix}${theme.tree.hook} ${theme.fg("dim", sanitizeText(recent.tool))}`;
|
|
934
939
|
const toolDetail = progress.lastIntent ?? recent.args;
|
|
935
940
|
if (toolDetail) {
|
|
936
|
-
toolLine += `: ${theme.fg("dim", previewLine(toolDetail, 40))}`;
|
|
941
|
+
toolLine += `: ${theme.fg("dim", previewLine(sanitizeText(toolDetail), 40))}`;
|
|
937
942
|
}
|
|
938
943
|
lines.push(toolLine);
|
|
939
944
|
}
|
|
@@ -947,12 +952,12 @@ function renderAgentProgress(
|
|
|
947
952
|
const waitLabel = remainingMs > 0 ? `in ${formatDuration(remainingMs)}` : "now";
|
|
948
953
|
const summary =
|
|
949
954
|
`retrying ${progress.retryState.attempt}/${progress.retryState.maxAttempts} ${waitLabel}: ` +
|
|
950
|
-
previewLine(progress.retryState.errorMessage, 60);
|
|
955
|
+
previewLine(sanitizeText(progress.retryState.errorMessage), 60);
|
|
951
956
|
lines.push(`${continuePrefix}${theme.tree.hook} ${theme.fg("warning", summary)}`);
|
|
952
957
|
} else if (progress.retryFailure && progress.status !== "running") {
|
|
953
958
|
const summary = `auto-retry gave up after ${progress.retryFailure.attempt} attempt${
|
|
954
959
|
progress.retryFailure.attempt === 1 ? "" : "s"
|
|
955
|
-
}: ${previewLine(progress.retryFailure.errorMessage, 80)}`;
|
|
960
|
+
}: ${previewLine(sanitizeText(progress.retryFailure.errorMessage), 80)}`;
|
|
956
961
|
lines.push(`${continuePrefix}${theme.tree.hook} ${theme.fg("error", summary)}`);
|
|
957
962
|
}
|
|
958
963
|
|
|
@@ -1098,13 +1103,13 @@ function renderReviewResult(
|
|
|
1098
1103
|
if (summary.explanation) {
|
|
1099
1104
|
if (expanded) {
|
|
1100
1105
|
lines.push(`${continuePrefix}${theme.fg("dim", "Summary")}`);
|
|
1101
|
-
const explanationLines = summary.explanation.split("\n");
|
|
1106
|
+
const explanationLines = sanitizeText(summary.explanation).split("\n");
|
|
1102
1107
|
for (const line of explanationLines) {
|
|
1103
1108
|
lines.push(`${continuePrefix} ${theme.fg("dim", replaceTabs(line))}`);
|
|
1104
1109
|
}
|
|
1105
1110
|
} else {
|
|
1106
1111
|
// Preview: first sentence or ~100 chars (flatten tabs/newlines first)
|
|
1107
|
-
const flat = replaceTabs(summary.explanation).replace(/[\r\n]+/g, " ");
|
|
1112
|
+
const flat = replaceTabs(sanitizeText(summary.explanation)).replace(/[\r\n]+/g, " ");
|
|
1108
1113
|
const firstSentence = flat.split(/[.!?]/)[0].trim();
|
|
1109
1114
|
const preview = truncateToWidth(`${firstSentence}.`, 100);
|
|
1110
1115
|
lines.push(`${continuePrefix}${theme.fg("dim", preview)}`);
|
|
@@ -1145,9 +1150,9 @@ function renderFindings(
|
|
|
1145
1150
|
const findingContinue = isLastFinding ? " " : `${theme.tree.vertical} `;
|
|
1146
1151
|
|
|
1147
1152
|
const { color } = getPriorityInfo(finding.priority);
|
|
1148
|
-
const rawTitle = finding.title?.replace(/^\[P\d\]\s*/, "") ?? "Untitled";
|
|
1153
|
+
const rawTitle = sanitizeText(finding.title?.replace(/^\[P\d\]\s*/, "") ?? "Untitled");
|
|
1149
1154
|
const titleText = replaceTabs(rawTitle).replace(/[\r\n]+/g, " ");
|
|
1150
|
-
const loc = `${path.basename(finding.file_path || "<unknown>")}:${finding.line_start}`;
|
|
1155
|
+
const loc = `${path.basename(sanitizeText(finding.file_path || "<unknown>"))}:${finding.line_start}`;
|
|
1151
1156
|
|
|
1152
1157
|
lines.push(
|
|
1153
1158
|
`${continuePrefix}${findingPrefix} ${theme.fg(color, `[${finding.priority}]`)} ${titleText} ${theme.fg("dim", loc)}`,
|
|
@@ -1156,7 +1161,7 @@ function renderFindings(
|
|
|
1156
1161
|
// Show body when expanded
|
|
1157
1162
|
if (expanded && finding.body) {
|
|
1158
1163
|
// Wrap body text
|
|
1159
|
-
const bodyLines = finding.body.split("\n");
|
|
1164
|
+
const bodyLines = sanitizeText(finding.body).split("\n");
|
|
1160
1165
|
for (const bodyLine of bodyLines) {
|
|
1161
1166
|
lines.push(`${continuePrefix}${findingContinue}${theme.fg("dim", replaceTabs(bodyLine))}`);
|
|
1162
1167
|
}
|
|
@@ -1208,7 +1213,7 @@ function renderAgentResult(
|
|
|
1208
1213
|
: "failed";
|
|
1209
1214
|
|
|
1210
1215
|
// Main status line: id: description [status] · stats · ⟨agent⟩
|
|
1211
|
-
const trimmedDescription = result.description
|
|
1216
|
+
const trimmedDescription = result.description ? sanitizeText(result.description).trim() : undefined;
|
|
1212
1217
|
const description = trimmedDescription ? previewLine(trimmedDescription, 64) : undefined;
|
|
1213
1218
|
const displayId = formatTaskId(result.id);
|
|
1214
1219
|
const titlePart = description ? `${theme.bold(displayId)}: ${description}` : displayId;
|
|
@@ -1242,7 +1247,7 @@ function renderAgentResult(
|
|
|
1242
1247
|
|
|
1243
1248
|
if (aborted && result.abortReason) {
|
|
1244
1249
|
lines.push(
|
|
1245
|
-
`${continuePrefix}${theme.fg("error", theme.status.aborted)} ${theme.fg("dim", previewLine(result.abortReason, 80))}`,
|
|
1250
|
+
`${continuePrefix}${theme.fg("error", theme.status.aborted)} ${theme.fg("dim", previewLine(sanitizeText(result.abortReason), 80))}`,
|
|
1246
1251
|
);
|
|
1247
1252
|
}
|
|
1248
1253
|
// Check for review result, preferring incremental yield sections and falling
|
|
@@ -1346,7 +1351,7 @@ function renderAgentResult(
|
|
|
1346
1351
|
lines.push(
|
|
1347
1352
|
`${continuePrefix}${theme.fg("warning", theme.status.warning)} ${theme.fg(
|
|
1348
1353
|
"dim",
|
|
1349
|
-
truncateToWidth(missingCompleteWarning, 80),
|
|
1354
|
+
truncateToWidth(sanitizeText(missingCompleteWarning), 80),
|
|
1350
1355
|
)}`,
|
|
1351
1356
|
);
|
|
1352
1357
|
}
|
|
@@ -1370,7 +1375,9 @@ function renderAgentResult(
|
|
|
1370
1375
|
|
|
1371
1376
|
// Error message
|
|
1372
1377
|
if (result.error && (!success || mergeFailed) && (!aborted || result.error !== result.abortReason)) {
|
|
1373
|
-
lines.push(
|
|
1378
|
+
lines.push(
|
|
1379
|
+
`${continuePrefix}${theme.fg(mergeFailed ? "warning" : "error", previewLine(sanitizeText(result.error), 70))}`,
|
|
1380
|
+
);
|
|
1374
1381
|
}
|
|
1375
1382
|
|
|
1376
1383
|
return lines;
|
package/src/tools/bash.ts
CHANGED
|
@@ -23,7 +23,6 @@ import { CachedOutputBlock, markFramedBlockComponent, outputBlockContentWidth }
|
|
|
23
23
|
import { getSixelLineMask } from "../utils/sixel";
|
|
24
24
|
import type { ToolSession } from ".";
|
|
25
25
|
import { truncateForPrompt } from "./approval";
|
|
26
|
-
import { applyBashFixups } from "./bash-command-fixup";
|
|
27
26
|
import { type BashInteractiveResult, runInteractiveBashPty } from "./bash-interactive";
|
|
28
27
|
import { checkBashInterception } from "./bash-interceptor";
|
|
29
28
|
import { canUseInteractiveBashPty } from "./bash-pty-selection";
|
|
@@ -660,16 +659,6 @@ export class BashTool implements AgentTool<typeof bashSchemaBase | typeof bashSc
|
|
|
660
659
|
let command = rawCommand;
|
|
661
660
|
const env = normalizeBashEnv(rawEnv);
|
|
662
661
|
|
|
663
|
-
// Apply conservative bash fixups (strip trailing `| head|tail` and redundant
|
|
664
|
-
// `2>&1`). The helper is single-line only and refuses anything that could
|
|
665
|
-
// change semantics.
|
|
666
|
-
if (this.session.settings.get("bash.stripTrailingHeadTail")) {
|
|
667
|
-
const fixup = applyBashFixups(command);
|
|
668
|
-
if (fixup.stripped.length > 0) {
|
|
669
|
-
command = fixup.command;
|
|
670
|
-
}
|
|
671
|
-
}
|
|
672
|
-
|
|
673
662
|
// Extract leading `cd <path> && ...` into cwd when the model ignores the cwd parameter.
|
|
674
663
|
// Constrained to a single line so a `&&` that sits on a later line of a multiline
|
|
675
664
|
// script can't pull the entire script into the "cwd" capture.
|
package/src/tools/path-utils.ts
CHANGED
|
@@ -147,8 +147,13 @@ export function expandTilde(filePath: string, home?: string): string {
|
|
|
147
147
|
}
|
|
148
148
|
|
|
149
149
|
export function expandPath(filePath: string): string {
|
|
150
|
+
// Some models intermittently prefix an otherwise-valid path with a stray
|
|
151
|
+
// `:` (e.g. `:/abs/path`, `:../rel`). No real path starts with `:` and it
|
|
152
|
+
// never begins a selector against an absolute/relative path, so strip it
|
|
153
|
+
// before resolution — mirroring the `@`-prefix normalization above.
|
|
154
|
+
const deColoned = /^:(?=[/~]|\.\.?\/)/.test(filePath) ? filePath.slice(1) : filePath;
|
|
150
155
|
const normalized = stripWindowsExtendedLengthPathPrefix(
|
|
151
|
-
stripFileUrl(normalizeUnicodeSpaces(normalizeAtPrefix(
|
|
156
|
+
stripFileUrl(normalizeUnicodeSpaces(normalizeAtPrefix(deColoned))),
|
|
152
157
|
);
|
|
153
158
|
return expandTilde(normalized);
|
|
154
159
|
}
|
|
@@ -52,6 +52,8 @@ const THINKING_TAG_ENVELOPE_RE = /<(think|thinking|reasoning)>\s*[\s\S]*?<\/\1>/
|
|
|
52
52
|
const THINKING_FENCE_ENVELOPE_RE = /```(?:thinking|reasoning)\b[\s\S]*?```/gi;
|
|
53
53
|
const LEADING_THINKING_TAG_RE = /^\s*<(think|thinking|reasoning)>\s*[\s\S]*?<\/\1>\s*/i;
|
|
54
54
|
const LEADING_THINKING_FENCE_RE = /^\s*```(?:thinking|reasoning)\b[\s\S]*?```\s*/i;
|
|
55
|
+
const LEADING_PROSE_THINKING_PREAMBLE_RE =
|
|
56
|
+
/^[ \t]*(?:(?:here(?:['’]s| is)[ \t]+(?:a|the|my)[ \t]+)|my[ \t]+)?(?:thinking|thought|reasoning)[ \t]+process[ \t]*:?[ \t]*(?:\r?\n|$)/i;
|
|
55
57
|
|
|
56
58
|
/**
|
|
57
59
|
* Whether the model honors a forced `tool_choice` so the `set_title` tool can be
|
|
@@ -300,9 +302,11 @@ function extractGeneratedTitle(contentBlocks: AssistantMessage["content"]): stri
|
|
|
300
302
|
// leaked thinking envelopes plus any stray/unclosed title tag fragment.
|
|
301
303
|
const markedTitle = extractVisibleMarkedTitle(textTitle);
|
|
302
304
|
if (markedTitle !== undefined) return markedTitle;
|
|
303
|
-
|
|
305
|
+
const cleanedTextTitle = stripLeadingLeakedThinkingMarkup(textTitle)
|
|
304
306
|
.replace(/<\/?title>/gi, "")
|
|
305
307
|
.trim();
|
|
308
|
+
if (LEADING_PROSE_THINKING_PREAMBLE_RE.test(cleanedTextTitle)) return "";
|
|
309
|
+
return cleanedTextTitle;
|
|
306
310
|
}
|
|
307
311
|
|
|
308
312
|
function extractVisibleMarkedTitle(text: string): string | undefined {
|
|
@@ -1,11 +0,0 @@
|
|
|
1
|
-
export interface BashFixupResult {
|
|
2
|
-
/** Possibly-rewritten command. */
|
|
3
|
-
command: string;
|
|
4
|
-
/** Substrings that were stripped, in the order they were removed. */
|
|
5
|
-
stripped: string[];
|
|
6
|
-
}
|
|
7
|
-
/**
|
|
8
|
-
* Apply both fixups to a bash command. On any parse failure, multi-line input,
|
|
9
|
-
* or no-op transform, returns the input verbatim with `stripped: []`.
|
|
10
|
-
*/
|
|
11
|
-
export declare function applyBashFixups(command: string): BashFixupResult;
|
|
@@ -1,37 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Conservative transforms applied to a bash command before execution.
|
|
3
|
-
*
|
|
4
|
-
* Two fixups are applied, each anchored to the end of a top-level segment
|
|
5
|
-
* (segments split on `;`, `&&`, `||`, and background `&`):
|
|
6
|
-
*
|
|
7
|
-
* 1. Trailing `| head [args]` / `| tail [args]` (and the `|&` variant) — these
|
|
8
|
-
* pipes exist purely to limit output length. The harness already truncates
|
|
9
|
-
* bash output and exposes the full result via an artifact, so they only
|
|
10
|
-
* hide content the agent wanted.
|
|
11
|
-
*
|
|
12
|
-
* 2. A redundant trailing `2>&1` left on a segment that has no remaining pipe
|
|
13
|
-
* or other redirect. The harness already merges stderr into stdout, so the
|
|
14
|
-
* duplication is purely cosmetic — and often a leftover after fixup (1)
|
|
15
|
-
* drops a downstream pipe.
|
|
16
|
-
*
|
|
17
|
-
* The heavy lifting (tokenization, quoting, heredoc handling, command
|
|
18
|
-
* substitution, nested compound commands) lives in Rust under
|
|
19
|
-
* `pi_shell::fixup`, driven by the real `brush-parser` AST. This module is a
|
|
20
|
-
* thin sync wrapper plus user-facing notice formatting.
|
|
21
|
-
*/
|
|
22
|
-
import { applyBashFixups as nativeApplyBashFixups } from "jeopi-natives";
|
|
23
|
-
|
|
24
|
-
export interface BashFixupResult {
|
|
25
|
-
/** Possibly-rewritten command. */
|
|
26
|
-
command: string;
|
|
27
|
-
/** Substrings that were stripped, in the order they were removed. */
|
|
28
|
-
stripped: string[];
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
/**
|
|
32
|
-
* Apply both fixups to a bash command. On any parse failure, multi-line input,
|
|
33
|
-
* or no-op transform, returns the input verbatim with `stripped: []`.
|
|
34
|
-
*/
|
|
35
|
-
export function applyBashFixups(command: string): BashFixupResult {
|
|
36
|
-
return nativeApplyBashFixups(command);
|
|
37
|
-
}
|