@oh-my-pi/pi-coding-agent 14.9.1 → 14.9.2
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 +15 -0
- package/package.json +7 -7
- package/src/extensibility/extensions/runner.ts +173 -177
- package/src/hashline/apply.ts +8 -24
- package/src/hashline/execute.ts +0 -1
- package/src/hashline/grammar.lark +1 -5
- package/src/hashline/parser.ts +1 -40
- package/src/hashline/types.ts +1 -2
- package/src/mcp/transports/http.ts +49 -47
- package/src/prompts/tools/hashline.md +1 -4
- package/src/sdk.ts +12 -22
- package/src/system-prompt.ts +30 -95
- package/src/task/executor.ts +6 -4
- package/src/task/index.ts +0 -2
- package/src/task/render.ts +4 -2
- package/src/tools/index.ts +0 -3
- package/src/tools/read.ts +2 -22
- package/src/workspace-tree.ts +210 -410
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,21 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
## [14.9.2] - 2026-05-10
|
|
6
|
+
### Added
|
|
7
|
+
|
|
8
|
+
- Added `agentsMdFiles` to `WorkspaceTree` so AGENTS.md discovery results are returned with the workspace scan output
|
|
9
|
+
|
|
10
|
+
### Changed
|
|
11
|
+
|
|
12
|
+
- Changed startup workspace discovery to use one native `listWorkspace` walk for both the rendered tree and AGENTS.md directory-context candidates, removing the layered `git ls-files` orchestration and secondary AGENTS.md glob.
|
|
13
|
+
|
|
14
|
+
### Fixed
|
|
15
|
+
|
|
16
|
+
- Fixed AGENTS.md context discovery to include AGENTS.md files that are explicitly gitignored while still excluding AGENTS.md files under ignored directories
|
|
17
|
+
- Fixed task tool renderer spamming `Tool renderer failed: undefined is not an object (evaluating 'args.tasks.length')` warnings while a `task` call was streaming in (the `tasks` array is undefined until the partial JSON parser closes it); the renderer now tolerates an absent `tasks` field and shows `0 agents` until the array arrives ([#985](https://github.com/can1357/oh-my-pi/issues/985)).
|
|
18
|
+
- Fixed MCP HTTP streamable transport spamming `HTTP SSE stream error: ReadableStream already has a controller` after every JSON-RPC request whose response was returned as `text/event-stream`. The transport used to break out of the SSE iterator once the matching response was captured and then re-open `response.body` for a background drain, but the body had already been piped through a `TransformStream` and could not be re-read. The drain now runs from a single iterator that resolves the response promise inline and continues to dispatch piggybacked notifications on the same stream.
|
|
19
|
+
|
|
5
20
|
## [14.9.0] - 2026-05-10
|
|
6
21
|
### Breaking Changes
|
|
7
22
|
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"type": "module",
|
|
3
3
|
"name": "@oh-my-pi/pi-coding-agent",
|
|
4
|
-
"version": "14.9.
|
|
4
|
+
"version": "14.9.2",
|
|
5
5
|
"description": "Coding agent CLI with read, bash, edit, write tools and session management",
|
|
6
6
|
"homepage": "https://github.com/can1357/oh-my-pi",
|
|
7
7
|
"author": "Can Boluk",
|
|
@@ -47,12 +47,12 @@
|
|
|
47
47
|
"@agentclientprotocol/sdk": "0.21.0",
|
|
48
48
|
"@babel/parser": "^7.29.3",
|
|
49
49
|
"@mozilla/readability": "^0.6.0",
|
|
50
|
-
"@oh-my-pi/omp-stats": "14.9.
|
|
51
|
-
"@oh-my-pi/pi-agent-core": "14.9.
|
|
52
|
-
"@oh-my-pi/pi-ai": "14.9.
|
|
53
|
-
"@oh-my-pi/pi-natives": "14.9.
|
|
54
|
-
"@oh-my-pi/pi-tui": "14.9.
|
|
55
|
-
"@oh-my-pi/pi-utils": "14.9.
|
|
50
|
+
"@oh-my-pi/omp-stats": "14.9.2",
|
|
51
|
+
"@oh-my-pi/pi-agent-core": "14.9.2",
|
|
52
|
+
"@oh-my-pi/pi-ai": "14.9.2",
|
|
53
|
+
"@oh-my-pi/pi-natives": "14.9.2",
|
|
54
|
+
"@oh-my-pi/pi-tui": "14.9.2",
|
|
55
|
+
"@oh-my-pi/pi-utils": "14.9.2",
|
|
56
56
|
"@puppeteer/browsers": "^2.13.0",
|
|
57
57
|
"@sinclair/typebox": "^0.34.49",
|
|
58
58
|
"@types/turndown": "5.0.6",
|
|
@@ -60,6 +60,15 @@ interface BeforeAgentStartCombinedResult {
|
|
|
60
60
|
|
|
61
61
|
export type ExtensionErrorListener = (error: ExtensionError) => void;
|
|
62
62
|
|
|
63
|
+
export const EXTENSION_HANDLER_TIMEOUT_MS = 30_000;
|
|
64
|
+
let extensionHandlerTimeoutMs = EXTENSION_HANDLER_TIMEOUT_MS;
|
|
65
|
+
|
|
66
|
+
export function __test_setExtensionHandlerTimeoutMs(timeoutMs: number): void {
|
|
67
|
+
extensionHandlerTimeoutMs = timeoutMs;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const EXTENSION_HANDLER_TIMEOUT = Symbol("extensionHandlerTimeout");
|
|
71
|
+
|
|
63
72
|
/**
|
|
64
73
|
* Events handled by the generic emit() method.
|
|
65
74
|
* Events with dedicated emitXxx() methods are excluded for stronger type safety.
|
|
@@ -434,6 +443,46 @@ export class ExtensionRunner {
|
|
|
434
443
|
);
|
|
435
444
|
}
|
|
436
445
|
|
|
446
|
+
async #runHandlerWithTimeout<TEvent extends { type: string }, TResult>(
|
|
447
|
+
handler: (event: TEvent, ctx: ExtensionContext) => Promise<TResult | undefined> | TResult | undefined,
|
|
448
|
+
event: TEvent,
|
|
449
|
+
ctx: ExtensionContext,
|
|
450
|
+
ext: Extension,
|
|
451
|
+
timeoutMs: number,
|
|
452
|
+
): Promise<TResult | undefined> {
|
|
453
|
+
try {
|
|
454
|
+
const handlerResult = await Promise.race([
|
|
455
|
+
Promise.resolve(handler(event, ctx)),
|
|
456
|
+
Bun.sleep(timeoutMs).then(() => EXTENSION_HANDLER_TIMEOUT),
|
|
457
|
+
]);
|
|
458
|
+
if (handlerResult === EXTENSION_HANDLER_TIMEOUT) {
|
|
459
|
+
const error = `handler timed out after ${timeoutMs}ms`;
|
|
460
|
+
logger.warn("Extension handler timed out", {
|
|
461
|
+
extensionPath: ext.path,
|
|
462
|
+
event: event.type,
|
|
463
|
+
timeoutMs,
|
|
464
|
+
});
|
|
465
|
+
this.emitError({
|
|
466
|
+
extensionPath: ext.path,
|
|
467
|
+
event: event.type,
|
|
468
|
+
error,
|
|
469
|
+
});
|
|
470
|
+
return undefined;
|
|
471
|
+
}
|
|
472
|
+
return handlerResult as TResult | undefined;
|
|
473
|
+
} catch (err) {
|
|
474
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
475
|
+
const stack = err instanceof Error ? err.stack : undefined;
|
|
476
|
+
this.emitError({
|
|
477
|
+
extensionPath: ext.path,
|
|
478
|
+
event: event.type,
|
|
479
|
+
error: message,
|
|
480
|
+
stack,
|
|
481
|
+
});
|
|
482
|
+
return undefined;
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
|
|
437
486
|
async emit<TEvent extends RunnerEmitEvent>(event: TEvent): Promise<RunnerEmitResult<TEvent>> {
|
|
438
487
|
const ctx = this.createContext();
|
|
439
488
|
let result: SessionBeforeEventResult | SessionCompactingResult | undefined;
|
|
@@ -443,28 +492,23 @@ export class ExtensionRunner {
|
|
|
443
492
|
if (!handlers || handlers.length === 0) continue;
|
|
444
493
|
|
|
445
494
|
for (const handler of handlers) {
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
495
|
+
const handlerResult = await this.#runHandlerWithTimeout(
|
|
496
|
+
handler,
|
|
497
|
+
event,
|
|
498
|
+
ctx,
|
|
499
|
+
ext,
|
|
500
|
+
extensionHandlerTimeoutMs,
|
|
501
|
+
);
|
|
502
|
+
|
|
503
|
+
if (this.#isSessionBeforeEvent(event) && handlerResult) {
|
|
504
|
+
result = handlerResult as SessionBeforeEventResult;
|
|
505
|
+
if (result.cancel) {
|
|
506
|
+
return result as RunnerEmitResult<TEvent>;
|
|
454
507
|
}
|
|
508
|
+
}
|
|
455
509
|
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
}
|
|
459
|
-
} catch (err) {
|
|
460
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
461
|
-
const stack = err instanceof Error ? err.stack : undefined;
|
|
462
|
-
this.emitError({
|
|
463
|
-
extensionPath: ext.path,
|
|
464
|
-
event: event.type,
|
|
465
|
-
error: message,
|
|
466
|
-
stack,
|
|
467
|
-
});
|
|
510
|
+
if (event.type === "session.compacting" && handlerResult) {
|
|
511
|
+
result = handlerResult as SessionCompactingResult;
|
|
468
512
|
}
|
|
469
513
|
}
|
|
470
514
|
}
|
|
@@ -482,31 +526,26 @@ export class ExtensionRunner {
|
|
|
482
526
|
if (!handlers || handlers.length === 0) continue;
|
|
483
527
|
|
|
484
528
|
for (const handler of handlers) {
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
}
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
extensionPath: ext.path,
|
|
506
|
-
event: "tool_result",
|
|
507
|
-
error: message,
|
|
508
|
-
stack,
|
|
509
|
-
});
|
|
529
|
+
const handlerResult = (await this.#runHandlerWithTimeout(
|
|
530
|
+
handler,
|
|
531
|
+
currentEvent,
|
|
532
|
+
ctx,
|
|
533
|
+
ext,
|
|
534
|
+
extensionHandlerTimeoutMs,
|
|
535
|
+
)) as ToolResultEventResult | undefined;
|
|
536
|
+
if (!handlerResult) continue;
|
|
537
|
+
|
|
538
|
+
if (handlerResult.content !== undefined) {
|
|
539
|
+
currentEvent.content = handlerResult.content;
|
|
540
|
+
modified = true;
|
|
541
|
+
}
|
|
542
|
+
if (handlerResult.details !== undefined) {
|
|
543
|
+
currentEvent.details = handlerResult.details;
|
|
544
|
+
modified = true;
|
|
545
|
+
}
|
|
546
|
+
if (handlerResult.isError !== undefined) {
|
|
547
|
+
currentEvent.isError = handlerResult.isError;
|
|
548
|
+
modified = true;
|
|
510
549
|
}
|
|
511
550
|
}
|
|
512
551
|
}
|
|
@@ -574,20 +613,15 @@ export class ExtensionRunner {
|
|
|
574
613
|
if (!handlers || handlers.length === 0) continue;
|
|
575
614
|
|
|
576
615
|
for (const handler of handlers) {
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
extensionPath: ext.path,
|
|
587
|
-
event: eventName,
|
|
588
|
-
error: message,
|
|
589
|
-
stack,
|
|
590
|
-
});
|
|
616
|
+
const handlerResult = await this.#runHandlerWithTimeout(
|
|
617
|
+
handler,
|
|
618
|
+
event,
|
|
619
|
+
ctx,
|
|
620
|
+
ext,
|
|
621
|
+
extensionHandlerTimeoutMs,
|
|
622
|
+
);
|
|
623
|
+
if (handlerResult) {
|
|
624
|
+
return handlerResult as R;
|
|
591
625
|
}
|
|
592
626
|
}
|
|
593
627
|
}
|
|
@@ -613,29 +647,24 @@ export class ExtensionRunner {
|
|
|
613
647
|
if (!handlers || handlers.length === 0) continue;
|
|
614
648
|
|
|
615
649
|
for (const handler of handlers) {
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
extensionPath: ext.path,
|
|
635
|
-
event: "resources_discover",
|
|
636
|
-
error: message,
|
|
637
|
-
stack,
|
|
638
|
-
});
|
|
650
|
+
const event: ResourcesDiscoverEvent = { type: "resources_discover", cwd, reason };
|
|
651
|
+
const handlerResult = await this.#runHandlerWithTimeout(
|
|
652
|
+
handler,
|
|
653
|
+
event,
|
|
654
|
+
ctx,
|
|
655
|
+
ext,
|
|
656
|
+
extensionHandlerTimeoutMs,
|
|
657
|
+
);
|
|
658
|
+
const result = handlerResult as ResourcesDiscoverResult | undefined;
|
|
659
|
+
|
|
660
|
+
if (result?.skillPaths?.length) {
|
|
661
|
+
skillPaths.push(...result.skillPaths.map(path => ({ path, extensionPath: ext.path })));
|
|
662
|
+
}
|
|
663
|
+
if (result?.promptPaths?.length) {
|
|
664
|
+
promptPaths.push(...result.promptPaths.map(path => ({ path, extensionPath: ext.path })));
|
|
665
|
+
}
|
|
666
|
+
if (result?.themePaths?.length) {
|
|
667
|
+
themePaths.push(...result.themePaths.map(path => ({ path, extensionPath: ext.path })));
|
|
639
668
|
}
|
|
640
669
|
}
|
|
641
670
|
}
|
|
@@ -655,21 +684,14 @@ export class ExtensionRunner {
|
|
|
655
684
|
|
|
656
685
|
for (const ext of this.extensions) {
|
|
657
686
|
for (const handler of ext.handlers.get("input") ?? []) {
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
} catch (err) {
|
|
667
|
-
this.emitError({
|
|
668
|
-
extensionPath: ext.path,
|
|
669
|
-
event: "input",
|
|
670
|
-
error: err instanceof Error ? err.message : String(err),
|
|
671
|
-
stack: err instanceof Error ? err.stack : undefined,
|
|
672
|
-
});
|
|
687
|
+
const event: InputEvent = { type: "input", text: currentText, images: currentImages, source };
|
|
688
|
+
const result = (await this.#runHandlerWithTimeout(handler, event, ctx, ext, extensionHandlerTimeoutMs)) as
|
|
689
|
+
| InputEventResult
|
|
690
|
+
| undefined;
|
|
691
|
+
if (result?.handled) return result;
|
|
692
|
+
if (result?.text !== undefined) {
|
|
693
|
+
currentText = result.text;
|
|
694
|
+
currentImages = result.images ?? currentImages;
|
|
673
695
|
}
|
|
674
696
|
}
|
|
675
697
|
}
|
|
@@ -704,22 +726,17 @@ export class ExtensionRunner {
|
|
|
704
726
|
if (!handlers || handlers.length === 0) continue;
|
|
705
727
|
|
|
706
728
|
for (const handler of handlers) {
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
extensionPath: ext.path,
|
|
719
|
-
event: "context",
|
|
720
|
-
error: message,
|
|
721
|
-
stack,
|
|
722
|
-
});
|
|
729
|
+
const event: ContextEvent = { type: "context", messages: currentMessages };
|
|
730
|
+
const handlerResult = await this.#runHandlerWithTimeout(
|
|
731
|
+
handler,
|
|
732
|
+
event,
|
|
733
|
+
ctx,
|
|
734
|
+
ext,
|
|
735
|
+
extensionHandlerTimeoutMs,
|
|
736
|
+
);
|
|
737
|
+
|
|
738
|
+
if (handlerResult && (handlerResult as ContextEventResult).messages) {
|
|
739
|
+
currentMessages = (handlerResult as ContextEventResult).messages!;
|
|
723
740
|
}
|
|
724
741
|
}
|
|
725
742
|
}
|
|
@@ -736,24 +753,19 @@ export class ExtensionRunner {
|
|
|
736
753
|
if (!handlers || handlers.length === 0) continue;
|
|
737
754
|
|
|
738
755
|
for (const handler of handlers) {
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
extensionPath: ext.path,
|
|
753
|
-
event: "before_provider_request",
|
|
754
|
-
error: message,
|
|
755
|
-
stack,
|
|
756
|
-
});
|
|
756
|
+
const event: BeforeProviderRequestEvent = {
|
|
757
|
+
type: "before_provider_request",
|
|
758
|
+
payload: currentPayload,
|
|
759
|
+
};
|
|
760
|
+
const handlerResult = await this.#runHandlerWithTimeout(
|
|
761
|
+
handler,
|
|
762
|
+
event,
|
|
763
|
+
ctx,
|
|
764
|
+
ext,
|
|
765
|
+
extensionHandlerTimeoutMs,
|
|
766
|
+
);
|
|
767
|
+
if (handlerResult !== undefined) {
|
|
768
|
+
currentPayload = handlerResult;
|
|
757
769
|
}
|
|
758
770
|
}
|
|
759
771
|
}
|
|
@@ -769,25 +781,14 @@ export class ExtensionRunner {
|
|
|
769
781
|
if (!handlers || handlers.length === 0) continue;
|
|
770
782
|
|
|
771
783
|
for (const handler of handlers) {
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
await handler(event, ctx);
|
|
781
|
-
} catch (err) {
|
|
782
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
783
|
-
const stack = err instanceof Error ? err.stack : undefined;
|
|
784
|
-
this.emitError({
|
|
785
|
-
extensionPath: ext.path,
|
|
786
|
-
event: "after_provider_response",
|
|
787
|
-
error: message,
|
|
788
|
-
stack,
|
|
789
|
-
});
|
|
790
|
-
}
|
|
784
|
+
const event: AfterProviderResponseEvent = {
|
|
785
|
+
type: "after_provider_response",
|
|
786
|
+
status: response.status,
|
|
787
|
+
headers: response.headers,
|
|
788
|
+
requestId: response.requestId,
|
|
789
|
+
metadata: response.metadata,
|
|
790
|
+
};
|
|
791
|
+
await this.#runHandlerWithTimeout(handler, event, ctx, ext, extensionHandlerTimeoutMs);
|
|
791
792
|
}
|
|
792
793
|
}
|
|
793
794
|
}
|
|
@@ -807,34 +808,29 @@ export class ExtensionRunner {
|
|
|
807
808
|
if (!handlers || handlers.length === 0) continue;
|
|
808
809
|
|
|
809
810
|
for (const handler of handlers) {
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
811
|
+
const event: BeforeAgentStartEvent = {
|
|
812
|
+
type: "before_agent_start",
|
|
813
|
+
prompt,
|
|
814
|
+
images,
|
|
815
|
+
systemPrompt: currentSystemPrompt,
|
|
816
|
+
};
|
|
817
|
+
const handlerResult = await this.#runHandlerWithTimeout(
|
|
818
|
+
handler,
|
|
819
|
+
event,
|
|
820
|
+
ctx,
|
|
821
|
+
ext,
|
|
822
|
+
extensionHandlerTimeoutMs,
|
|
823
|
+
);
|
|
824
|
+
|
|
825
|
+
if (handlerResult) {
|
|
826
|
+
const result = handlerResult as BeforeAgentStartEventResult;
|
|
827
|
+
if (result.message) {
|
|
828
|
+
messages.push(result.message);
|
|
829
|
+
}
|
|
830
|
+
if (result.systemPrompt !== undefined) {
|
|
831
|
+
currentSystemPrompt = result.systemPrompt;
|
|
832
|
+
systemPromptModified = true;
|
|
828
833
|
}
|
|
829
|
-
} catch (err) {
|
|
830
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
831
|
-
const stack = err instanceof Error ? err.stack : undefined;
|
|
832
|
-
this.emitError({
|
|
833
|
-
extensionPath: ext.path,
|
|
834
|
-
event: "before_agent_start",
|
|
835
|
-
error: message,
|
|
836
|
-
stack,
|
|
837
|
-
});
|
|
838
834
|
}
|
|
839
835
|
}
|
|
840
836
|
}
|
package/src/hashline/apply.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { HashlineMismatchError } from "./anchors";
|
|
2
2
|
import { RANGE_INTERIOR_HASH } from "./constants";
|
|
3
|
-
import { computeLineHash
|
|
3
|
+
import { computeLineHash } from "./hash";
|
|
4
4
|
import { cloneCursor } from "./parser";
|
|
5
5
|
import type { Anchor, HashlineApplyOptions, HashlineCursor, HashlineEdit, HashMismatch } from "./types";
|
|
6
6
|
|
|
@@ -37,7 +37,6 @@ interface HashlineReplacementGroup {
|
|
|
37
37
|
|
|
38
38
|
function getHashlineEditAnchors(edit: HashlineEdit): Anchor[] {
|
|
39
39
|
if (edit.kind === "delete") return [edit.anchor];
|
|
40
|
-
if (edit.kind === "modify") return [edit.anchor];
|
|
41
40
|
if (edit.cursor.kind === "before_anchor") return [edit.cursor.anchor];
|
|
42
41
|
if (edit.cursor.kind === "after_anchor") return [edit.cursor.anchor];
|
|
43
42
|
return [];
|
|
@@ -96,7 +95,7 @@ function insertAtEnd(fileLines: string[], lineOrigins: HashlineLineOrigin[], lin
|
|
|
96
95
|
/** Bucket edits by the line they target so we can apply each line's group in one splice. */
|
|
97
96
|
|
|
98
97
|
function getAnchorTargetLine(edit: HashlineEdit): number | undefined {
|
|
99
|
-
if (edit.kind === "delete"
|
|
98
|
+
if (edit.kind === "delete") return edit.anchor.line;
|
|
100
99
|
if (edit.cursor.kind === "before_anchor" || edit.cursor.kind === "after_anchor") return edit.cursor.anchor.line;
|
|
101
100
|
return undefined;
|
|
102
101
|
}
|
|
@@ -607,11 +606,9 @@ function bucketAnchorEditsByLine(edits: IndexedEdit[]): Map<number, IndexedEdit[
|
|
|
607
606
|
const line =
|
|
608
607
|
entry.edit.kind === "delete"
|
|
609
608
|
? entry.edit.anchor.line
|
|
610
|
-
: entry.edit.kind === "
|
|
611
|
-
? entry.edit.anchor.line
|
|
612
|
-
:
|
|
613
|
-
? entry.edit.cursor.anchor.line
|
|
614
|
-
: 0;
|
|
609
|
+
: entry.edit.cursor.kind === "before_anchor"
|
|
610
|
+
? entry.edit.cursor.anchor.line
|
|
611
|
+
: 0;
|
|
615
612
|
const bucket = byLine.get(line);
|
|
616
613
|
if (bucket) bucket.push(entry);
|
|
617
614
|
else byLine.set(line, [entry]);
|
|
@@ -683,33 +680,20 @@ export function applyHashlineEdits(
|
|
|
683
680
|
const currentLine = fileLines[idx] ?? "";
|
|
684
681
|
const beforeLines: string[] = [];
|
|
685
682
|
let deleteLine = false;
|
|
686
|
-
let prefix = "";
|
|
687
|
-
let suffix = "";
|
|
688
|
-
let modified = false;
|
|
689
683
|
|
|
690
684
|
for (const { edit } of bucket) {
|
|
691
685
|
if (edit.kind === "insert") {
|
|
692
686
|
beforeLines.push(edit.text);
|
|
693
687
|
} else if (edit.kind === "delete") {
|
|
694
688
|
deleteLine = true;
|
|
695
|
-
} else if (edit.kind === "modify") {
|
|
696
|
-
prefix = edit.prefix + prefix;
|
|
697
|
-
suffix = suffix + edit.suffix;
|
|
698
|
-
modified = true;
|
|
699
689
|
}
|
|
700
690
|
}
|
|
701
|
-
if (beforeLines.length === 0 && !deleteLine
|
|
702
|
-
if (deleteLine && modified) {
|
|
703
|
-
throw new Error(
|
|
704
|
-
`line ${line}: cannot combine inline modify ("< ${line}${HL_EDIT_SEP}…" or "+ ${line}${HL_EDIT_SEP}…") with a delete or replace targeting the same line.`,
|
|
705
|
-
);
|
|
706
|
-
}
|
|
691
|
+
if (beforeLines.length === 0 && !deleteLine) continue;
|
|
707
692
|
|
|
708
|
-
const
|
|
709
|
-
const replacement = deleteLine ? beforeLines : [...beforeLines, effectiveLine];
|
|
693
|
+
const replacement = deleteLine ? beforeLines : [...beforeLines, currentLine];
|
|
710
694
|
const origins = replacement.map((): HashlineLineOrigin => (deleteLine ? "replacement" : "insert"));
|
|
711
695
|
if (!deleteLine) {
|
|
712
|
-
origins[origins.length - 1] =
|
|
696
|
+
origins[origins.length - 1] = lineOrigins[idx] ?? "original";
|
|
713
697
|
}
|
|
714
698
|
|
|
715
699
|
fileLines.splice(idx, 1, ...replacement);
|
package/src/hashline/execute.ts
CHANGED
|
@@ -42,7 +42,6 @@ async function readHashlineFile(absolutePath: string, pathText: string): Promise
|
|
|
42
42
|
function hasAnchorScopedEdit(edits: HashlineEdit[]): boolean {
|
|
43
43
|
return edits.some(edit => {
|
|
44
44
|
if (edit.kind === "delete") return true;
|
|
45
|
-
if (edit.kind === "modify") return true;
|
|
46
45
|
return edit.cursor.kind === "before_anchor" || edit.cursor.kind === "after_anchor";
|
|
47
46
|
});
|
|
48
47
|
}
|
|
@@ -7,16 +7,12 @@ section: file_header line_op*
|
|
|
7
7
|
|
|
8
8
|
file_header: "@" path LF
|
|
9
9
|
|
|
10
|
-
line_op:
|
|
11
|
-
| inline_after_op payload*
|
|
12
|
-
| insert_before_op payload+
|
|
10
|
+
line_op: insert_before_op payload+
|
|
13
11
|
| insert_after_op payload+
|
|
14
12
|
| replace_op payload*
|
|
15
13
|
| delete_op
|
|
16
14
|
| blank
|
|
17
15
|
|
|
18
|
-
inline_before_op: "<" LID $HSEP$ line_text? LF
|
|
19
|
-
inline_after_op: "+" LID $HSEP$ line_text? LF
|
|
20
16
|
insert_before_op: "<" insert_target LF
|
|
21
17
|
insert_after_op: "+" insert_target LF
|
|
22
18
|
replace_op: "=" range LF
|
package/src/hashline/parser.ts
CHANGED
|
@@ -1,9 +1,8 @@
|
|
|
1
1
|
import { RANGE_INTERIOR_HASH } from "./constants";
|
|
2
|
-
import { describeAnchorExamples, HL_EDIT_SEP,
|
|
2
|
+
import { describeAnchorExamples, HL_EDIT_SEP, HL_HASH_CAPTURE_RE_RAW } from "./hash";
|
|
3
3
|
import type { Anchor, HashlineCursor, HashlineEdit } from "./types";
|
|
4
4
|
import { stripTrailingCarriageReturn } from "./utils";
|
|
5
5
|
|
|
6
|
-
const HL_EDIT_SEPARATOR_RE = HL_EDIT_SEP_RE_RAW;
|
|
7
6
|
const LID_CAPTURE_RE = new RegExp(`^${HL_HASH_CAPTURE_RE_RAW}$`);
|
|
8
7
|
|
|
9
8
|
function parseLid(raw: string, lineNum: number): Anchor {
|
|
@@ -70,8 +69,6 @@ const INSERT_BEFORE_OP_RE = /^<\s*(\S+)$/;
|
|
|
70
69
|
const INSERT_AFTER_OP_RE = /^\+\s*(\S+)$/;
|
|
71
70
|
const DELETE_OP_RE = /^-\s*(\S+)$/;
|
|
72
71
|
const REPLACE_OP_RE = /^=\s*(\S+)$/;
|
|
73
|
-
const INLINE_BEFORE_OP_RE = new RegExp(`^<\\s*${HL_HASH_CAPTURE_RE_RAW}${HL_EDIT_SEPARATOR_RE}(.*)$`);
|
|
74
|
-
const INLINE_AFTER_OP_RE = new RegExp(`^\\+\\s*${HL_HASH_CAPTURE_RE_RAW}${HL_EDIT_SEPARATOR_RE}(.*)$`);
|
|
75
72
|
|
|
76
73
|
export function cloneCursor(cursor: HashlineCursor): HashlineCursor {
|
|
77
74
|
if (cursor.kind === "before_anchor") return { kind: "before_anchor", anchor: { ...cursor.anchor } };
|
|
@@ -125,42 +122,6 @@ export function parseHashlineWithWarnings(diff: string): { edits: HashlineEdit[]
|
|
|
125
122
|
throw new Error(`line ${lineNum}: payload line has no preceding +, <, or = operation.`);
|
|
126
123
|
}
|
|
127
124
|
|
|
128
|
-
const inlineBeforeMatch = INLINE_BEFORE_OP_RE.exec(line);
|
|
129
|
-
if (inlineBeforeMatch) {
|
|
130
|
-
const anchor = parseLid(`${inlineBeforeMatch[1]}${inlineBeforeMatch[2]}`, lineNum);
|
|
131
|
-
edits.push({
|
|
132
|
-
kind: "modify",
|
|
133
|
-
anchor,
|
|
134
|
-
prefix: inlineBeforeMatch[3],
|
|
135
|
-
suffix: "",
|
|
136
|
-
lineNum,
|
|
137
|
-
index: editIndex++,
|
|
138
|
-
});
|
|
139
|
-
const cursor: HashlineCursor = { kind: "before_anchor", anchor };
|
|
140
|
-
const { payload, nextIndex } = collectPayload(lines, i + 1, lineNum, false);
|
|
141
|
-
for (const text of payload) pushInsert(cursor, text, lineNum);
|
|
142
|
-
i = nextIndex;
|
|
143
|
-
continue;
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
const inlineAfterMatch = INLINE_AFTER_OP_RE.exec(line);
|
|
147
|
-
if (inlineAfterMatch) {
|
|
148
|
-
const anchor = parseLid(`${inlineAfterMatch[1]}${inlineAfterMatch[2]}`, lineNum);
|
|
149
|
-
edits.push({
|
|
150
|
-
kind: "modify",
|
|
151
|
-
anchor,
|
|
152
|
-
prefix: "",
|
|
153
|
-
suffix: inlineAfterMatch[3],
|
|
154
|
-
lineNum,
|
|
155
|
-
index: editIndex++,
|
|
156
|
-
});
|
|
157
|
-
const cursor: HashlineCursor = { kind: "after_anchor", anchor };
|
|
158
|
-
const { payload, nextIndex } = collectPayload(lines, i + 1, lineNum, false);
|
|
159
|
-
for (const text of payload) pushInsert(cursor, text, lineNum);
|
|
160
|
-
i = nextIndex;
|
|
161
|
-
continue;
|
|
162
|
-
}
|
|
163
|
-
|
|
164
125
|
const insertBeforeMatch = INSERT_BEFORE_OP_RE.exec(line);
|
|
165
126
|
if (insertBeforeMatch) {
|
|
166
127
|
const cursor = parseInsertTarget(insertBeforeMatch[1], lineNum, "before");
|
package/src/hashline/types.ts
CHANGED
|
@@ -24,8 +24,7 @@ export type HashlineCursor =
|
|
|
24
24
|
|
|
25
25
|
export type HashlineEdit =
|
|
26
26
|
| { kind: "insert"; cursor: HashlineCursor; text: string; lineNum: number; index: number }
|
|
27
|
-
| { kind: "delete"; anchor: Anchor; lineNum: number; index: number; oldAssertion?: string }
|
|
28
|
-
| { kind: "modify"; anchor: Anchor; prefix: string; suffix: string; lineNum: number; index: number };
|
|
27
|
+
| { kind: "delete"; anchor: Anchor; lineNum: number; index: number; oldAssertion?: string };
|
|
29
28
|
|
|
30
29
|
export const hashlineEditParamsSchema = Type.Object({ input: Type.String() });
|
|
31
30
|
export type HashlineParams = Static<typeof hashlineEditParamsSchema>;
|