@oh-my-pi/pi-coding-agent 15.9.3 → 15.9.67
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 +74 -1
- package/dist/types/cli/classify-install-target.d.ts +5 -1
- package/dist/types/config/keybindings.d.ts +4 -1
- package/dist/types/config/settings-schema.d.ts +24 -5
- package/dist/types/edit/file-snapshot-store.d.ts +1 -1
- package/dist/types/eval/__tests__/kernel-spawn.test.d.ts +1 -0
- package/dist/types/eval/backend.d.ts +6 -6
- package/dist/types/eval/bridge-timeout.d.ts +27 -0
- package/dist/types/eval/idle-timeout.d.ts +16 -14
- package/dist/types/eval/js/executor.d.ts +3 -3
- package/dist/types/eval/py/executor.d.ts +2 -2
- package/dist/types/eval/py/spawn-options.d.ts +58 -0
- package/dist/types/modes/components/assistant-message.d.ts +16 -0
- package/dist/types/modes/components/copy-selector.d.ts +22 -0
- package/dist/types/modes/components/custom-editor.d.ts +3 -1
- package/dist/types/modes/components/error-banner.d.ts +11 -0
- package/dist/types/modes/components/model-selector.d.ts +1 -0
- package/dist/types/modes/components/tool-execution.d.ts +15 -0
- package/dist/types/modes/components/transcript-container.d.ts +1 -0
- package/dist/types/modes/components/user-message.d.ts +1 -1
- package/dist/types/modes/controllers/command-controller.d.ts +0 -1
- package/dist/types/modes/controllers/selector-controller.d.ts +1 -0
- package/dist/types/modes/image-references.d.ts +17 -0
- package/dist/types/modes/interactive-mode.d.ts +8 -1
- package/dist/types/modes/types.d.ts +8 -1
- package/dist/types/modes/utils/copy-targets.d.ts +53 -0
- package/dist/types/modes/utils/ui-helpers.d.ts +1 -0
- package/dist/types/session/blob-store.d.ts +12 -11
- package/dist/types/session/session-manager.d.ts +5 -3
- package/dist/types/system-prompt.d.ts +2 -0
- package/dist/types/tiny/title-client.d.ts +16 -1
- package/dist/types/tool-discovery/mode.d.ts +8 -0
- package/dist/types/tools/archive-reader.d.ts +5 -1
- package/dist/types/tools/eval-render.d.ts +8 -0
- package/dist/types/tools/render-utils.d.ts +25 -0
- package/dist/types/tui/code-cell.d.ts +6 -0
- package/dist/types/tui/hyperlink.d.ts +12 -0
- package/dist/types/tui/output-block.d.ts +11 -0
- package/dist/types/web/search/render.d.ts +1 -2
- package/package.json +9 -9
- package/src/autoresearch/dashboard.ts +11 -21
- package/src/cli/classify-install-target.ts +31 -5
- package/src/cli/claude-trace-cli.ts +13 -1
- package/src/cli/plugin-cli.ts +45 -0
- package/src/cli/web-search-cli.ts +0 -1
- package/src/config/keybindings.ts +58 -1
- package/src/config/model-registry.ts +54 -4
- package/src/config/settings-schema.ts +25 -5
- package/src/debug/raw-sse.ts +18 -4
- package/src/edit/file-snapshot-store.ts +1 -1
- package/src/edit/index.ts +1 -1
- package/src/edit/renderer.ts +7 -7
- package/src/edit/streaming.ts +1 -1
- package/src/eval/__tests__/agent-bridge.test.ts +100 -27
- package/src/eval/__tests__/bridge-timeout.test.ts +64 -0
- package/src/eval/__tests__/idle-timeout.test.ts +26 -12
- package/src/eval/__tests__/kernel-spawn.test.ts +103 -0
- package/src/eval/__tests__/llm-bridge.test.ts +10 -10
- package/src/eval/__tests__/shared-executors.test.ts +2 -2
- package/src/eval/agent-bridge.ts +4 -5
- package/src/eval/backend.ts +6 -6
- package/src/eval/bridge-timeout.ts +44 -0
- package/src/eval/idle-timeout.ts +33 -15
- package/src/eval/js/executor.ts +10 -10
- package/src/eval/llm-bridge.ts +4 -5
- package/src/eval/py/executor.ts +6 -6
- package/src/eval/py/kernel.ts +11 -1
- package/src/eval/py/spawn-options.ts +126 -0
- package/src/eval/py/tool-bridge.ts +43 -5
- package/src/export/ttsr.ts +9 -0
- package/src/extensibility/custom-commands/bundled/ci-green/index.ts +31 -2
- package/src/extensibility/extensions/runner.ts +2 -0
- package/src/internal-urls/docs-index.generated.ts +9 -8
- package/src/lsp/client.ts +80 -2
- package/src/lsp/index.ts +38 -4
- package/src/lsp/render.ts +3 -3
- package/src/main.ts +8 -2
- package/src/modes/components/agent-dashboard.ts +13 -4
- package/src/modes/components/assistant-message.ts +44 -1
- package/src/modes/components/copy-selector.ts +249 -0
- package/src/modes/components/custom-editor.ts +14 -2
- package/src/modes/components/error-banner.ts +33 -0
- package/src/modes/components/extensions/extension-list.ts +17 -8
- package/src/modes/components/history-search.ts +19 -11
- package/src/modes/components/model-selector.ts +125 -29
- package/src/modes/components/oauth-selector.ts +28 -12
- package/src/modes/components/session-observer-overlay.ts +13 -15
- package/src/modes/components/session-selector.ts +24 -13
- package/src/modes/components/tool-execution.ts +71 -13
- package/src/modes/components/transcript-container.ts +93 -32
- package/src/modes/components/tree-selector.ts +19 -7
- package/src/modes/components/user-message-selector.ts +25 -14
- package/src/modes/components/user-message.ts +9 -2
- package/src/modes/controllers/command-controller.ts +0 -116
- package/src/modes/controllers/event-controller.ts +67 -12
- package/src/modes/controllers/input-controller.ts +33 -1
- package/src/modes/controllers/selector-controller.ts +38 -1
- package/src/modes/image-references.ts +111 -0
- package/src/modes/interactive-mode.ts +52 -17
- package/src/modes/theme/theme.ts +46 -10
- package/src/modes/types.ts +11 -2
- package/src/modes/utils/copy-targets.ts +254 -0
- package/src/modes/utils/ui-helpers.ts +23 -2
- package/src/prompts/ci-green-request.md +5 -3
- package/src/prompts/system/project-prompt.md +1 -0
- package/src/prompts/tools/ast-edit.md +1 -1
- package/src/prompts/tools/ast-grep.md +1 -1
- package/src/prompts/tools/read.md +1 -1
- package/src/prompts/tools/search.md +1 -1
- package/src/sdk.ts +17 -9
- package/src/session/agent-session.ts +43 -14
- package/src/session/blob-store.ts +96 -9
- package/src/session/session-manager.ts +19 -10
- package/src/slash-commands/builtin-registry.ts +3 -11
- package/src/system-prompt.ts +4 -0
- package/src/task/render.ts +38 -11
- package/src/tiny/title-client.ts +7 -1
- package/src/tool-discovery/mode.ts +24 -0
- package/src/tools/archive-reader.ts +339 -31
- package/src/tools/bash.ts +18 -8
- package/src/tools/browser/render.ts +5 -4
- package/src/tools/debug.ts +3 -3
- package/src/tools/eval-render.ts +24 -9
- package/src/tools/eval.ts +14 -19
- package/src/tools/fetch.ts +34 -14
- package/src/tools/gh.ts +65 -11
- package/src/tools/index.ts +6 -8
- package/src/tools/read.ts +65 -19
- package/src/tools/render-utils.ts +46 -0
- package/src/tools/search-tool-bm25.ts +4 -6
- package/src/tools/search.ts +60 -11
- package/src/tools/ssh.ts +21 -8
- package/src/tools/write.ts +17 -8
- package/src/tui/code-cell.ts +19 -4
- package/src/tui/hyperlink.ts +42 -7
- package/src/tui/output-block.ts +14 -0
- package/src/web/search/index.ts +2 -2
- package/src/web/search/render.ts +23 -55
- package/dist/types/eval/heartbeat.d.ts +0 -45
- package/src/eval/__tests__/heartbeat.test.ts +0 -84
- package/src/eval/heartbeat.ts +0 -74
- /package/dist/types/eval/__tests__/{heartbeat.test.d.ts → bridge-timeout.test.d.ts} +0 -0
|
@@ -96,6 +96,7 @@ import type { AssistantMessageComponent } from "./components/assistant-message";
|
|
|
96
96
|
import type { BashExecutionComponent } from "./components/bash-execution";
|
|
97
97
|
import { CustomEditor } from "./components/custom-editor";
|
|
98
98
|
import { DynamicBorder } from "./components/dynamic-border";
|
|
99
|
+
import { ErrorBannerComponent } from "./components/error-banner";
|
|
99
100
|
import type { EvalExecutionComponent } from "./components/eval-execution";
|
|
100
101
|
import type { HookEditorComponent } from "./components/hook-editor";
|
|
101
102
|
import type { HookInputComponent } from "./components/hook-input";
|
|
@@ -264,6 +265,7 @@ export class InteractiveMode implements InteractiveModeContext {
|
|
|
264
265
|
todoContainer: Container;
|
|
265
266
|
btwContainer: Container;
|
|
266
267
|
omfgContainer: Container;
|
|
268
|
+
errorBannerContainer: Container;
|
|
267
269
|
editor: CustomEditor;
|
|
268
270
|
editorContainer: Container;
|
|
269
271
|
hookWidgetContainerAbove: Container;
|
|
@@ -288,6 +290,7 @@ export class InteractiveMode implements InteractiveModeContext {
|
|
|
288
290
|
todoPhases: TodoPhase[] = [];
|
|
289
291
|
hideThinkingBlock = false;
|
|
290
292
|
pendingImages: ImageContent[] = [];
|
|
293
|
+
pendingImageLinks: (string | undefined)[] = [];
|
|
291
294
|
compactionQueuedMessages: CompactionQueuedMessage[] = [];
|
|
292
295
|
pendingTools = new Map<string, ToolExecutionHandle>();
|
|
293
296
|
pendingBashComponents: BashExecutionComponent[] = [];
|
|
@@ -404,6 +407,7 @@ export class InteractiveMode implements InteractiveModeContext {
|
|
|
404
407
|
this.todoContainer = new Container();
|
|
405
408
|
this.btwContainer = new Container();
|
|
406
409
|
this.omfgContainer = new Container();
|
|
410
|
+
this.errorBannerContainer = new Container();
|
|
407
411
|
this.editor = new CustomEditor(getEditorTheme());
|
|
408
412
|
this.editor.setUseTerminalCursor(this.ui.getShowHardwareCursor());
|
|
409
413
|
this.editor.setAutocompleteMaxVisible(settings.get("autocompleteMaxVisible"));
|
|
@@ -565,6 +569,7 @@ export class InteractiveMode implements InteractiveModeContext {
|
|
|
565
569
|
this.ui.addChild(this.todoContainer);
|
|
566
570
|
this.ui.addChild(this.btwContainer);
|
|
567
571
|
this.ui.addChild(this.omfgContainer);
|
|
572
|
+
this.ui.addChild(this.errorBannerContainer);
|
|
568
573
|
this.ui.addChild(this.statusLine); // Only renders hook statuses (main status in editor border)
|
|
569
574
|
this.ui.addChild(this.hookWidgetContainerAbove);
|
|
570
575
|
this.ui.addChild(this.editorContainer);
|
|
@@ -896,12 +901,14 @@ export class InteractiveMode implements InteractiveModeContext {
|
|
|
896
901
|
startPendingSubmission(input: {
|
|
897
902
|
text: string;
|
|
898
903
|
images?: ImageContent[];
|
|
904
|
+
imageLinks?: (string | undefined)[];
|
|
899
905
|
customType?: string;
|
|
900
906
|
display?: boolean;
|
|
901
907
|
}): SubmittedUserInput {
|
|
902
908
|
const submission: SubmittedUserInput = {
|
|
903
909
|
text: input.text,
|
|
904
910
|
images: input.images,
|
|
911
|
+
imageLinks: input.imageLinks,
|
|
905
912
|
customType: input.customType,
|
|
906
913
|
display: input.display,
|
|
907
914
|
cancelled: false,
|
|
@@ -913,22 +920,28 @@ export class InteractiveMode implements InteractiveModeContext {
|
|
|
913
920
|
const imageCount = submission.images?.length ?? 0;
|
|
914
921
|
this.optimisticUserMessageSignature = `${submission.text}\u0000${imageCount}`;
|
|
915
922
|
this.#pendingSubmissionDispose = this.recordLocalSubmission(submission.text, imageCount);
|
|
916
|
-
this.addMessageToChat(
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
923
|
+
this.addMessageToChat(
|
|
924
|
+
{
|
|
925
|
+
role: "user",
|
|
926
|
+
content: [{ type: "text", text: submission.text }, ...(submission.images ?? [])],
|
|
927
|
+
attribution: "user",
|
|
928
|
+
timestamp: Date.now(),
|
|
929
|
+
},
|
|
930
|
+
{ imageLinks: input.imageLinks },
|
|
931
|
+
);
|
|
922
932
|
} else {
|
|
923
933
|
this.optimisticUserMessageSignature = undefined;
|
|
924
934
|
this.#pendingSubmissionDispose = undefined;
|
|
925
935
|
}
|
|
926
936
|
this.editor.setText("");
|
|
927
|
-
|
|
928
|
-
//
|
|
929
|
-
//
|
|
930
|
-
|
|
931
|
-
|
|
937
|
+
this.editor.imageLinks = undefined;
|
|
938
|
+
// Reconciliation checkpoint: only retire frozen block snapshots after TUI
|
|
939
|
+
// proves the native viewport is at the tail and replays scrollback safely.
|
|
940
|
+
// Unknown host viewports stay frozen; thawing them would expose live rows
|
|
941
|
+
// over stale native history and can yank or duplicate when ED3 is unsafe.
|
|
942
|
+
if (this.ui.refreshNativeScrollbackIfDirty()) {
|
|
943
|
+
this.chatContainer.thaw();
|
|
944
|
+
}
|
|
932
945
|
this.ensureLoadingAnimation();
|
|
933
946
|
this.ui.requestRender();
|
|
934
947
|
return submission;
|
|
@@ -956,6 +969,8 @@ export class InteractiveMode implements InteractiveModeContext {
|
|
|
956
969
|
}
|
|
957
970
|
if (!submission.customType) {
|
|
958
971
|
this.pendingImages = submission.images ? [...submission.images] : [];
|
|
972
|
+
this.pendingImageLinks = submission.imageLinks ? [...submission.imageLinks] : [];
|
|
973
|
+
this.editor.imageLinks = this.pendingImageLinks;
|
|
959
974
|
this.rebuildChatFromMessages();
|
|
960
975
|
this.editor.setText(submission.text);
|
|
961
976
|
}
|
|
@@ -2492,6 +2507,19 @@ export class InteractiveMode implements InteractiveModeContext {
|
|
|
2492
2507
|
this.#uiHelpers.showError(message);
|
|
2493
2508
|
}
|
|
2494
2509
|
|
|
2510
|
+
showPinnedError(message: string): void {
|
|
2511
|
+
if (this.isBackgrounded) return;
|
|
2512
|
+
this.errorBannerContainer.clear();
|
|
2513
|
+
this.errorBannerContainer.addChild(new ErrorBannerComponent(message));
|
|
2514
|
+
this.ui.requestRender();
|
|
2515
|
+
}
|
|
2516
|
+
|
|
2517
|
+
clearPinnedError(): void {
|
|
2518
|
+
if (this.errorBannerContainer.children.length === 0) return;
|
|
2519
|
+
this.errorBannerContainer.clear();
|
|
2520
|
+
this.ui.requestRender();
|
|
2521
|
+
}
|
|
2522
|
+
|
|
2495
2523
|
showWarning(message: string): void {
|
|
2496
2524
|
this.#uiHelpers.showWarning(message);
|
|
2497
2525
|
}
|
|
@@ -2622,7 +2650,10 @@ export class InteractiveMode implements InteractiveModeContext {
|
|
|
2622
2650
|
return this.#uiHelpers.isKnownSlashCommand(text);
|
|
2623
2651
|
}
|
|
2624
2652
|
|
|
2625
|
-
addMessageToChat(
|
|
2653
|
+
addMessageToChat(
|
|
2654
|
+
message: AgentMessage,
|
|
2655
|
+
options?: { populateHistory?: boolean; imageLinks?: readonly (string | undefined)[] },
|
|
2656
|
+
): Component[] {
|
|
2626
2657
|
return this.#uiHelpers.addMessageToChat(message, options);
|
|
2627
2658
|
}
|
|
2628
2659
|
|
|
@@ -2633,7 +2664,10 @@ export class InteractiveMode implements InteractiveModeContext {
|
|
|
2633
2664
|
this.#uiHelpers.renderSessionContext(sessionContext, options);
|
|
2634
2665
|
}
|
|
2635
2666
|
|
|
2636
|
-
renderInitialMessages(
|
|
2667
|
+
renderInitialMessages(
|
|
2668
|
+
prebuiltContext?: SessionContext,
|
|
2669
|
+
options?: { preserveExistingChat?: boolean; clearTerminalHistory?: boolean },
|
|
2670
|
+
): void {
|
|
2637
2671
|
this.#uiHelpers.renderInitialMessages(prebuiltContext, options);
|
|
2638
2672
|
}
|
|
2639
2673
|
|
|
@@ -2666,10 +2700,6 @@ export class InteractiveMode implements InteractiveModeContext {
|
|
|
2666
2700
|
return this.#commandController.handleShareCommand();
|
|
2667
2701
|
}
|
|
2668
2702
|
|
|
2669
|
-
handleCopyCommand(sub?: string) {
|
|
2670
|
-
return this.#commandController.handleCopyCommand(sub);
|
|
2671
|
-
}
|
|
2672
|
-
|
|
2673
2703
|
handleTodoCommand(args: string): Promise<void> {
|
|
2674
2704
|
return this.#todoCommandController.handleTodoCommand(args);
|
|
2675
2705
|
}
|
|
@@ -2706,6 +2736,7 @@ export class InteractiveMode implements InteractiveModeContext {
|
|
|
2706
2736
|
this.#btwController.dispose();
|
|
2707
2737
|
this.#omfgController.dispose();
|
|
2708
2738
|
this.#extensionUiController.clearExtensionTerminalInputListeners();
|
|
2739
|
+
this.clearPinnedError();
|
|
2709
2740
|
this.#planReviewContainer = undefined;
|
|
2710
2741
|
}
|
|
2711
2742
|
|
|
@@ -2901,6 +2932,10 @@ export class InteractiveMode implements InteractiveModeContext {
|
|
|
2901
2932
|
this.#selectorController.showUserMessageSelector();
|
|
2902
2933
|
}
|
|
2903
2934
|
|
|
2935
|
+
showCopySelector(): void {
|
|
2936
|
+
this.#selectorController.showCopySelector();
|
|
2937
|
+
}
|
|
2938
|
+
|
|
2904
2939
|
showTreeSelector(): void {
|
|
2905
2940
|
this.#selectorController.showTreeSelector();
|
|
2906
2941
|
}
|
package/src/modes/theme/theme.ts
CHANGED
|
@@ -12,6 +12,7 @@ import {
|
|
|
12
12
|
import type { EditorTheme, MarkdownTheme, SelectListTheme, SymbolTheme } from "@oh-my-pi/pi-tui";
|
|
13
13
|
import { adjustHsv, colorLuma, getCustomThemesDir, isEnoent, logger, relativeLuminance } from "@oh-my-pi/pi-utils";
|
|
14
14
|
import chalk from "chalk";
|
|
15
|
+
import { LRUCache } from "lru-cache/raw";
|
|
15
16
|
import * as z from "zod/v4";
|
|
16
17
|
// Embed theme JSON files at build time
|
|
17
18
|
import darkThemeJson from "./dark.json" with { type: "json" };
|
|
@@ -2429,17 +2430,54 @@ function getHighlightColors(t: Theme): NativeHighlightColors {
|
|
|
2429
2430
|
return cachedHighlightColors;
|
|
2430
2431
|
}
|
|
2431
2432
|
|
|
2433
|
+
/**
|
|
2434
|
+
* Memoized native syntax highlight. Returns the joined ANSI string, or `null`
|
|
2435
|
+
* when the native tokenizer throws so callers can apply their own fallback.
|
|
2436
|
+
*
|
|
2437
|
+
* Keyed on `(lang, code)` and reset whenever the active `theme` instance
|
|
2438
|
+
* changes — the ANSI colors are baked into the highlighted output, so a theme
|
|
2439
|
+
* switch (which always reassigns `theme`) must invalidate every entry.
|
|
2440
|
+
*
|
|
2441
|
+
* Why this exists: animated tool blocks (eval/bash) repaint their box on every
|
|
2442
|
+
* ~16ms border-shimmer frame, and markdown re-lexes on every streamed delta.
|
|
2443
|
+
* Without memoization each frame re-tokenizes an unchanged code body through the
|
|
2444
|
+
* Rust FFI — ~26ms for 100 lines, ~40ms for 150 — overrunning the 16ms frame
|
|
2445
|
+
* budget and starving the spinner/render timers (the "TUI freeze").
|
|
2446
|
+
*/
|
|
2447
|
+
const HIGHLIGHT_CACHE_MAX = 256;
|
|
2448
|
+
const highlightCache = new LRUCache<string, string>({ max: HIGHLIGHT_CACHE_MAX });
|
|
2449
|
+
let highlightCacheTheme: Theme | undefined;
|
|
2450
|
+
|
|
2451
|
+
function highlightCached(code: string, validLang: string | undefined): string | null {
|
|
2452
|
+
if (highlightCacheTheme !== theme) {
|
|
2453
|
+
highlightCache.clear();
|
|
2454
|
+
highlightCacheTheme = theme;
|
|
2455
|
+
}
|
|
2456
|
+
const key = `${validLang ?? ""}\x00${code}`;
|
|
2457
|
+
const hit = highlightCache.get(key);
|
|
2458
|
+
if (hit !== undefined) {
|
|
2459
|
+
return hit;
|
|
2460
|
+
}
|
|
2461
|
+
let highlighted: string;
|
|
2462
|
+
try {
|
|
2463
|
+
highlighted = nativeHighlightCode(code, validLang, getHighlightColors(theme));
|
|
2464
|
+
} catch {
|
|
2465
|
+
return null;
|
|
2466
|
+
}
|
|
2467
|
+
highlightCache.set(key, highlighted);
|
|
2468
|
+
return highlighted;
|
|
2469
|
+
}
|
|
2470
|
+
|
|
2432
2471
|
/**
|
|
2433
2472
|
* Highlight code with syntax coloring based on file extension or language.
|
|
2434
2473
|
* Returns array of highlighted lines.
|
|
2435
2474
|
*/
|
|
2436
2475
|
export function highlightCode(code: string, lang?: string): string[] {
|
|
2437
2476
|
const validLang = lang && nativeSupportsLanguage(lang) ? lang : undefined;
|
|
2438
|
-
|
|
2439
|
-
|
|
2440
|
-
|
|
2441
|
-
|
|
2442
|
-
}
|
|
2477
|
+
const highlighted = highlightCached(code, validLang);
|
|
2478
|
+
// Always return a fresh array: callers (e.g. renderCodeCell) push extra lines
|
|
2479
|
+
// onto the result, which would corrupt the cached string otherwise.
|
|
2480
|
+
return (highlighted ?? code).split("\n");
|
|
2443
2481
|
}
|
|
2444
2482
|
|
|
2445
2483
|
export function getSymbolTheme(): SymbolTheme {
|
|
@@ -2484,11 +2522,9 @@ export function getMarkdownTheme(): MarkdownTheme {
|
|
|
2484
2522
|
resolveMermaidAscii,
|
|
2485
2523
|
highlightCode: (code: string, lang?: string): string[] => {
|
|
2486
2524
|
const validLang = lang && nativeSupportsLanguage(lang) ? lang : undefined;
|
|
2487
|
-
|
|
2488
|
-
|
|
2489
|
-
|
|
2490
|
-
return code.split("\n").map(line => theme.fg("mdCodeBlock", line));
|
|
2491
|
-
}
|
|
2525
|
+
const highlighted = highlightCached(code, validLang);
|
|
2526
|
+
if (highlighted !== null) return highlighted.split("\n");
|
|
2527
|
+
return code.split("\n").map(line => theme.fg("mdCodeBlock", line));
|
|
2492
2528
|
},
|
|
2493
2529
|
};
|
|
2494
2530
|
cachedMarkdownTheme = markdownTheme;
|
package/src/modes/types.ts
CHANGED
|
@@ -40,6 +40,7 @@ export type CompactionQueuedMessage = {
|
|
|
40
40
|
export type SubmittedUserInput = {
|
|
41
41
|
text: string;
|
|
42
42
|
images?: ImageContent[];
|
|
43
|
+
imageLinks?: (string | undefined)[];
|
|
43
44
|
customType?: string;
|
|
44
45
|
display?: boolean;
|
|
45
46
|
cancelled: boolean;
|
|
@@ -75,6 +76,7 @@ export interface InteractiveModeContext {
|
|
|
75
76
|
todoContainer: Container;
|
|
76
77
|
btwContainer: Container;
|
|
77
78
|
omfgContainer: Container;
|
|
79
|
+
errorBannerContainer: Container;
|
|
78
80
|
editor: CustomEditor;
|
|
79
81
|
editorContainer: Container;
|
|
80
82
|
hookWidgetContainerAbove: Container;
|
|
@@ -106,6 +108,7 @@ export interface InteractiveModeContext {
|
|
|
106
108
|
planModePlanFilePath?: string;
|
|
107
109
|
hideThinkingBlock: boolean;
|
|
108
110
|
pendingImages: ImageContent[];
|
|
111
|
+
pendingImageLinks: (string | undefined)[];
|
|
109
112
|
compactionQueuedMessages: CompactionQueuedMessage[];
|
|
110
113
|
pendingTools: Map<string, ToolExecutionHandle>;
|
|
111
114
|
pendingBashComponents: BashExecutionComponent[];
|
|
@@ -157,6 +160,8 @@ export interface InteractiveModeContext {
|
|
|
157
160
|
// UI helpers
|
|
158
161
|
showStatus(message: string, options?: { dim?: boolean }): void;
|
|
159
162
|
showError(message: string): void;
|
|
163
|
+
showPinnedError(message: string): void;
|
|
164
|
+
clearPinnedError(): void;
|
|
160
165
|
showWarning(message: string): void;
|
|
161
166
|
showNewVersionNotification(newVersion: string): void;
|
|
162
167
|
clearEditor(): void;
|
|
@@ -171,6 +176,7 @@ export interface InteractiveModeContext {
|
|
|
171
176
|
startPendingSubmission(input: {
|
|
172
177
|
text: string;
|
|
173
178
|
images?: ImageContent[];
|
|
179
|
+
imageLinks?: (string | undefined)[];
|
|
174
180
|
customType?: string;
|
|
175
181
|
display?: boolean;
|
|
176
182
|
}): SubmittedUserInput;
|
|
@@ -191,7 +197,10 @@ export interface InteractiveModeContext {
|
|
|
191
197
|
*/
|
|
192
198
|
withLocalSubmission<T>(text: string, fn: () => Promise<T>, options?: { imageCount?: number }): Promise<T>;
|
|
193
199
|
isKnownSlashCommand(text: string): boolean;
|
|
194
|
-
addMessageToChat(
|
|
200
|
+
addMessageToChat(
|
|
201
|
+
message: AgentMessage,
|
|
202
|
+
options?: { populateHistory?: boolean; imageLinks?: readonly (string | undefined)[] },
|
|
203
|
+
): Component[];
|
|
195
204
|
renderSessionContext(
|
|
196
205
|
sessionContext: SessionContext,
|
|
197
206
|
options?: { updateFooter?: boolean; populateHistory?: boolean },
|
|
@@ -213,7 +222,6 @@ export interface InteractiveModeContext {
|
|
|
213
222
|
// Command handling
|
|
214
223
|
handleExportCommand(text: string): Promise<void>;
|
|
215
224
|
handleShareCommand(): Promise<void>;
|
|
216
|
-
handleCopyCommand(sub?: string): void;
|
|
217
225
|
handleTodoCommand(args: string): Promise<void>;
|
|
218
226
|
handleSessionCommand(): Promise<void>;
|
|
219
227
|
handleJobsCommand(): Promise<void>;
|
|
@@ -254,6 +262,7 @@ export interface InteractiveModeContext {
|
|
|
254
262
|
showModelSelector(options?: { temporaryOnly?: boolean }): void;
|
|
255
263
|
showPluginSelector(mode?: "install" | "uninstall"): void;
|
|
256
264
|
showUserMessageSelector(): void;
|
|
265
|
+
showCopySelector(): void;
|
|
257
266
|
showTreeSelector(): void;
|
|
258
267
|
showSessionSelector(): void;
|
|
259
268
|
handleResumeSession(sessionPath: string): Promise<void>;
|
|
@@ -0,0 +1,254 @@
|
|
|
1
|
+
import type { AgentMessage } from "@oh-my-pi/pi-agent-core";
|
|
2
|
+
import type { ToolCall } from "@oh-my-pi/pi-ai";
|
|
3
|
+
|
|
4
|
+
/** A fenced code block extracted from assistant markdown. */
|
|
5
|
+
export interface CodeBlock {
|
|
6
|
+
/** Info string after the opening fence (language id), trimmed. */
|
|
7
|
+
lang: string;
|
|
8
|
+
/** Block body with the trailing newline stripped. */
|
|
9
|
+
code: string;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/** A runnable command found in the transcript. */
|
|
13
|
+
export interface LastCommand {
|
|
14
|
+
kind: "bash" | "eval";
|
|
15
|
+
code: string;
|
|
16
|
+
/** Highlight language: "bash" for bash, "python"/"javascript" for eval. */
|
|
17
|
+
language: string;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* A node in the `/copy` picker tree. Leaves carry `content` (placed on the
|
|
22
|
+
* clipboard) plus `copyMessage` (the status shown afterwards); groups carry
|
|
23
|
+
* `children` to drill into.
|
|
24
|
+
*/
|
|
25
|
+
export interface CopyTarget {
|
|
26
|
+
/** Stable identifier (e.g. "msg:1", "msg:1:code:0", "msg:1:all", "cmd:1"). */
|
|
27
|
+
id: string;
|
|
28
|
+
label: string;
|
|
29
|
+
/** Dim annotation: line/block counts, language, or tool name. */
|
|
30
|
+
hint?: string;
|
|
31
|
+
/** Full text rendered in the preview pane. */
|
|
32
|
+
preview: string;
|
|
33
|
+
/** Highlight language for code/command previews (undefined = plain/markdown). */
|
|
34
|
+
language?: string;
|
|
35
|
+
/** Leaf: text copied to the clipboard. */
|
|
36
|
+
content?: string;
|
|
37
|
+
/** Leaf: status message shown after copying. */
|
|
38
|
+
copyMessage?: string;
|
|
39
|
+
/** Group: nested targets to drill into. */
|
|
40
|
+
children?: CopyTarget[];
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** Minimal session surface needed to assemble copy targets (eases testing). */
|
|
44
|
+
export interface CopySource {
|
|
45
|
+
readonly messages: readonly AgentMessage[];
|
|
46
|
+
getLastVisibleHandoffText(): string | undefined;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** Cap on how many recent assistant messages the picker lists. */
|
|
50
|
+
const MAX_MESSAGES = 50;
|
|
51
|
+
|
|
52
|
+
const CODE_BLOCK_RE = /^```([^\n]*)\n([\s\S]*?)^```/gm;
|
|
53
|
+
|
|
54
|
+
/** Extract fenced code blocks from assistant markdown, in document order. */
|
|
55
|
+
export function extractCodeBlocks(text: string): CodeBlock[] {
|
|
56
|
+
const blocks: CodeBlock[] = [];
|
|
57
|
+
for (const match of text.matchAll(CODE_BLOCK_RE)) {
|
|
58
|
+
blocks.push({ lang: match[1].trim(), code: match[2].replace(/\n$/, "") });
|
|
59
|
+
}
|
|
60
|
+
return blocks;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function extractEvalCode(args: unknown): { code: string; language: string } | undefined {
|
|
64
|
+
if (!args || typeof args !== "object") return undefined;
|
|
65
|
+
const cells = (args as { cells?: unknown }).cells;
|
|
66
|
+
if (!Array.isArray(cells)) return undefined;
|
|
67
|
+
|
|
68
|
+
const codeBlocks: string[] = [];
|
|
69
|
+
let language = "python";
|
|
70
|
+
let languageResolved = false;
|
|
71
|
+
for (const cell of cells) {
|
|
72
|
+
if (!cell || typeof cell !== "object") continue;
|
|
73
|
+
const code = (cell as { code?: unknown }).code;
|
|
74
|
+
if (typeof code !== "string" || code.length === 0) continue;
|
|
75
|
+
codeBlocks.push(code);
|
|
76
|
+
if (!languageResolved) {
|
|
77
|
+
language = (cell as { language?: unknown }).language === "js" ? "javascript" : "python";
|
|
78
|
+
languageResolved = true;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
return codeBlocks.length > 0 ? { code: codeBlocks.join("\n\n"), language } : undefined;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function commandFromToolCall(tc: ToolCall): LastCommand | undefined {
|
|
86
|
+
if (tc.name === "bash" && typeof tc.arguments.command === "string") {
|
|
87
|
+
return { kind: "bash", code: tc.arguments.command, language: "bash" };
|
|
88
|
+
}
|
|
89
|
+
if (tc.name === "eval") {
|
|
90
|
+
const evalResult = extractEvalCode(tc.arguments);
|
|
91
|
+
if (evalResult) return { kind: "eval", code: evalResult.code, language: evalResult.language };
|
|
92
|
+
}
|
|
93
|
+
return undefined;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/** Walk the transcript backwards for the most recent bash command or eval code. */
|
|
97
|
+
export function extractLastCommand(messages: readonly AgentMessage[]): LastCommand | undefined {
|
|
98
|
+
for (let i = messages.length - 1; i >= 0; i--) {
|
|
99
|
+
const msg = messages[i];
|
|
100
|
+
if (msg.role !== "assistant") continue;
|
|
101
|
+
const toolCalls = msg.content.filter((c): c is ToolCall => c.type === "toolCall");
|
|
102
|
+
for (let j = toolCalls.length - 1; j >= 0; j--) {
|
|
103
|
+
const command = commandFromToolCall(toolCalls[j]!);
|
|
104
|
+
if (command) return command;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
return undefined;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/** Concatenated visible text of an assistant message, or undefined when empty. */
|
|
111
|
+
function assistantText(msg: AgentMessage): string | undefined {
|
|
112
|
+
if (msg.role !== "assistant") return undefined;
|
|
113
|
+
let text = "";
|
|
114
|
+
for (const content of msg.content) {
|
|
115
|
+
if (content.type === "text") text += content.text;
|
|
116
|
+
}
|
|
117
|
+
return text.trim() || undefined;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function pluralLines(text: string): string {
|
|
121
|
+
const count = text.length === 0 ? 0 : text.split("\n").length;
|
|
122
|
+
return `${count} line${count === 1 ? "" : "s"}`;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function blockHint(block: CodeBlock): string {
|
|
126
|
+
const lines = pluralLines(block.code);
|
|
127
|
+
return block.lang ? `${block.lang} · ${lines}` : lines;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/** First non-empty line, whitespace-collapsed, used as a message label. */
|
|
131
|
+
function firstLine(text: string): string {
|
|
132
|
+
for (const line of text.split("\n")) {
|
|
133
|
+
const trimmed = line.trim();
|
|
134
|
+
if (trimmed) return trimmed.replace(/\s+/g, " ");
|
|
135
|
+
}
|
|
136
|
+
return text.trim().replace(/\s+/g, " ");
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/** Build the target node for one assistant message: a leaf when it has no code
|
|
140
|
+
* blocks, otherwise a group exposing the full message, each block, and "all". */
|
|
141
|
+
function messageTarget(text: string, rank: number): CopyTarget {
|
|
142
|
+
const id = `msg:${rank}`;
|
|
143
|
+
const label = firstLine(text);
|
|
144
|
+
const blocks = extractCodeBlocks(text);
|
|
145
|
+
const hint = blocks.length > 0 ? `${pluralLines(text)} · ${blocks.length} code` : pluralLines(text);
|
|
146
|
+
const messageCopy = rank === 1 ? "Copied last message to clipboard" : "Copied message to clipboard";
|
|
147
|
+
|
|
148
|
+
if (blocks.length === 0) {
|
|
149
|
+
return { id, label, hint, preview: text, content: text, copyMessage: messageCopy };
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// The message node itself copies the full message; its code blocks are
|
|
153
|
+
// child copy targets you can expand into.
|
|
154
|
+
const children: CopyTarget[] = blocks.map((block, j) => ({
|
|
155
|
+
id: `${id}:code:${j}`,
|
|
156
|
+
label: `Block ${j + 1}`,
|
|
157
|
+
hint: blockHint(block),
|
|
158
|
+
preview: block.code,
|
|
159
|
+
language: block.lang || undefined,
|
|
160
|
+
content: block.code,
|
|
161
|
+
copyMessage: `Copied code block ${j + 1} to clipboard`,
|
|
162
|
+
}));
|
|
163
|
+
if (blocks.length > 1) {
|
|
164
|
+
const combined = blocks.map(b => b.code).join("\n\n");
|
|
165
|
+
children.push({
|
|
166
|
+
id: `${id}:all`,
|
|
167
|
+
label: `All ${blocks.length} blocks`,
|
|
168
|
+
hint: pluralLines(combined),
|
|
169
|
+
preview: combined,
|
|
170
|
+
content: combined,
|
|
171
|
+
copyMessage: `Copied ${blocks.length} code blocks to clipboard`,
|
|
172
|
+
});
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
return { id, label, hint, preview: text, content: text, copyMessage: messageCopy, children };
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
function commandTitle(command: LastCommand): string {
|
|
179
|
+
return command.kind === "bash" ? "Bash command" : "Eval code";
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
function commandTarget(command: LastCommand, rank: number): CopyTarget {
|
|
183
|
+
const title = commandTitle(command);
|
|
184
|
+
return {
|
|
185
|
+
id: `cmd:${rank}`,
|
|
186
|
+
label: firstLine(command.code) || title,
|
|
187
|
+
hint: `${command.kind} · ${pluralLines(command.code)}`,
|
|
188
|
+
preview: command.code,
|
|
189
|
+
language: command.language,
|
|
190
|
+
content: command.code,
|
|
191
|
+
copyMessage: `Copied ${command.kind === "bash" ? "bash command" : "eval code"} to clipboard`,
|
|
192
|
+
};
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
* Assemble the unified `/copy` target tree: recent assistant messages
|
|
197
|
+
* (most recent first, each drillable into its code blocks), runnable command
|
|
198
|
+
* targets interleaved after the assistant message that issued them, and a
|
|
199
|
+
* fresh-handoff fallback when no assistant message exists yet.
|
|
200
|
+
*/
|
|
201
|
+
export function buildCopyTargets(source: CopySource): CopyTarget[] {
|
|
202
|
+
const targets: CopyTarget[] = [];
|
|
203
|
+
const pendingCommands: LastCommand[] = [];
|
|
204
|
+
let messageRank = 0;
|
|
205
|
+
let commandRank = 0;
|
|
206
|
+
|
|
207
|
+
const appendCommands = (commands: readonly LastCommand[]) => {
|
|
208
|
+
for (const command of commands) {
|
|
209
|
+
commandRank += 1;
|
|
210
|
+
targets.push(commandTarget(command, commandRank));
|
|
211
|
+
}
|
|
212
|
+
};
|
|
213
|
+
|
|
214
|
+
for (let i = source.messages.length - 1; i >= 0 && messageRank < MAX_MESSAGES; i--) {
|
|
215
|
+
const msg = source.messages[i];
|
|
216
|
+
if (msg.role !== "assistant") continue;
|
|
217
|
+
|
|
218
|
+
const toolCalls = msg.content.filter((c): c is ToolCall => c.type === "toolCall");
|
|
219
|
+
const commands: LastCommand[] = [];
|
|
220
|
+
for (let j = toolCalls.length - 1; j >= 0; j--) {
|
|
221
|
+
const command = commandFromToolCall(toolCalls[j]!);
|
|
222
|
+
if (command) commands.push(command);
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
const text = assistantText(msg);
|
|
226
|
+
if (!text) {
|
|
227
|
+
pendingCommands.push(...commands);
|
|
228
|
+
continue;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
messageRank += 1;
|
|
232
|
+
targets.push(messageTarget(text, messageRank));
|
|
233
|
+
appendCommands(pendingCommands);
|
|
234
|
+
appendCommands(commands);
|
|
235
|
+
pendingCommands.length = 0;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
if (messageRank === 0) {
|
|
239
|
+
const handoff = source.getLastVisibleHandoffText();
|
|
240
|
+
if (handoff) {
|
|
241
|
+
targets.unshift({
|
|
242
|
+
id: "handoff",
|
|
243
|
+
label: "Handoff context",
|
|
244
|
+
hint: pluralLines(handoff),
|
|
245
|
+
preview: handoff,
|
|
246
|
+
content: handoff,
|
|
247
|
+
copyMessage: "Copied handoff context to clipboard",
|
|
248
|
+
});
|
|
249
|
+
}
|
|
250
|
+
appendCommands(pendingCommands);
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
return targets;
|
|
254
|
+
}
|
|
@@ -18,6 +18,7 @@ import {
|
|
|
18
18
|
import { SkillMessageComponent } from "../../modes/components/skill-message";
|
|
19
19
|
import { ToolExecutionComponent } from "../../modes/components/tool-execution";
|
|
20
20
|
import { UserMessageComponent } from "../../modes/components/user-message";
|
|
21
|
+
import { materializeImageReferenceLinksSync } from "../../modes/image-references";
|
|
21
22
|
import { theme } from "../../modes/theme/theme";
|
|
22
23
|
import type { CompactionQueuedMessage, InteractiveModeContext } from "../../modes/types";
|
|
23
24
|
import {
|
|
@@ -40,6 +41,18 @@ type QueuedMessages = {
|
|
|
40
41
|
followUp: string[];
|
|
41
42
|
};
|
|
42
43
|
|
|
44
|
+
function imageLinksForMessage(
|
|
45
|
+
message: Extract<AgentMessage, { role: "developer" | "user" }>,
|
|
46
|
+
putBlobSync: InteractiveModeContext["sessionManager"]["putBlobSync"],
|
|
47
|
+
): (string | undefined)[] | undefined {
|
|
48
|
+
if (typeof message.content === "string") return undefined;
|
|
49
|
+
const images = message.content.filter(
|
|
50
|
+
(content): content is ImageContent =>
|
|
51
|
+
content.type === "image" && typeof content.data === "string" && typeof content.mimeType === "string",
|
|
52
|
+
);
|
|
53
|
+
return materializeImageReferenceLinksSync(images, putBlobSync);
|
|
54
|
+
}
|
|
55
|
+
|
|
43
56
|
export class UiHelpers {
|
|
44
57
|
constructor(private ctx: InteractiveModeContext) {}
|
|
45
58
|
|
|
@@ -84,7 +97,10 @@ export class UiHelpers {
|
|
|
84
97
|
this.ctx.ui.requestRender();
|
|
85
98
|
}
|
|
86
99
|
|
|
87
|
-
addMessageToChat(
|
|
100
|
+
addMessageToChat(
|
|
101
|
+
message: AgentMessage,
|
|
102
|
+
options?: { populateHistory?: boolean; imageLinks?: readonly (string | undefined)[] },
|
|
103
|
+
): Component[] {
|
|
88
104
|
switch (message.role) {
|
|
89
105
|
case "bashExecution": {
|
|
90
106
|
const component = new BashExecutionComponent(message.command, this.ctx.ui, message.excludeFromContext);
|
|
@@ -253,7 +269,10 @@ export class UiHelpers {
|
|
|
253
269
|
const textContent = this.ctx.getUserMessageText(message);
|
|
254
270
|
if (textContent) {
|
|
255
271
|
const isSynthetic = message.role === "developer" ? true : (message.synthetic ?? false);
|
|
256
|
-
const
|
|
272
|
+
const imageLinks =
|
|
273
|
+
options?.imageLinks ??
|
|
274
|
+
imageLinksForMessage(message, this.ctx.sessionManager.putBlobSync.bind(this.ctx.sessionManager));
|
|
275
|
+
const userComponent = new UserMessageComponent(textContent, isSynthetic, imageLinks);
|
|
257
276
|
this.ctx.chatContainer.addChild(userComponent);
|
|
258
277
|
if (options?.populateHistory && message.role === "user" && !isSynthetic) {
|
|
259
278
|
this.ctx.editor.addToHistory(textContent);
|
|
@@ -513,6 +532,8 @@ export class UiHelpers {
|
|
|
513
532
|
}
|
|
514
533
|
this.ctx.editor.setText("");
|
|
515
534
|
this.ctx.pendingImages = [];
|
|
535
|
+
this.ctx.pendingImageLinks = [];
|
|
536
|
+
this.ctx.editor.imageLinks = undefined;
|
|
516
537
|
this.ctx.ui.requestRender();
|
|
517
538
|
}
|
|
518
539
|
|
|
@@ -14,7 +14,7 @@ Do not stop after a single fix attempt.
|
|
|
14
14
|
2. If any run fails, inspect failing job output and logs.
|
|
15
15
|
3. Identify root cause and make minimal correct fix.
|
|
16
16
|
4. Run local verification if it reduces chance of another failing push.
|
|
17
|
-
5. Push the branch.
|
|
17
|
+
{{#if headTag}}5. Push the branch and tag `{{headTag}}` atomically: `git push --atomic "{{remote}}" "{{branch}}" "+refs/tags/{{headTag}}"`.{{else}}5. Push the branch.{{/if}}
|
|
18
18
|
6. Watch workflow runs for new HEAD commit again.
|
|
19
19
|
7. Repeat until workflow runs for latest HEAD commit succeed.
|
|
20
20
|
</procedure>
|
|
@@ -26,11 +26,13 @@ Do not stop after a single fix attempt.
|
|
|
26
26
|
|
|
27
27
|
{{#if headTag}}
|
|
28
28
|
<instruction>
|
|
29
|
-
|
|
29
|
+
Always push the branch and tag together atomically so the tag never points at an un-pushed or non-green commit:
|
|
30
|
+
`git push --atomic "{{remote}}" "{{branch}}" "+refs/tags/{{headTag}}"`.
|
|
31
|
+
The `--atomic` flag makes the branch and tag update succeed or fail as one ref transaction; `+refs/tags/{{headTag}}` force-moves the tag to the new HEAD. Do not push the branch first and retag later.
|
|
30
32
|
</instruction>
|
|
31
33
|
{{/if}}
|
|
32
34
|
|
|
33
35
|
<critical>
|
|
34
36
|
The task is complete only when the workflow runs for the latest HEAD commit succeed.
|
|
35
|
-
{{#if headTag}}The
|
|
37
|
+
{{#if headTag}}The latest HEAD commit must carry tag `{{headTag}}`, pushed atomically with the branch via `git push --atomic`.{{/if}}
|
|
36
38
|
</critical>
|
|
@@ -14,7 +14,7 @@ Performs structural AST-aware rewrites via native ast-grep.
|
|
|
14
14
|
</instruction>
|
|
15
15
|
|
|
16
16
|
<output>
|
|
17
|
-
- Replacement summary, per-file replacement counts, and change diffs as
|
|
17
|
+
- Replacement summary, per-file replacement counts, and change diffs as `[src/foo.ts#1A2B]`, `-12:before`, `+12:after` lines in hashline mode
|
|
18
18
|
- Parse issues when files cannot be processed
|
|
19
19
|
</output>
|
|
20
20
|
|