dsh-hashline-edit 0.2.0 → 0.2.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/README.md +59 -55
- package/cordis.patch.yml +1 -1
- package/lib/index.js +7225 -6445
- package/lib/types/dsh/contracts.d.ts +3 -12
- package/lib/types/dsh/fs.d.ts +0 -3
- package/lib/types/dsh/guidance.d.ts +5 -5
- package/lib/types/dsh/host-compat.d.ts +78 -7
- package/lib/types/dsh/index.d.ts +7 -17
- package/lib/types/dsh/metrics/events.d.ts +1 -1
- package/lib/types/dsh/metrics/observer.d.ts +1 -1
- package/lib/types/dsh/sandbox.d.ts +2 -2
- package/lib/types/dsh/shell-hook.d.ts +1 -1
- package/lib/types/dsh/tools/edit.d.ts +10 -0
- package/lib/types/dsh/tools/read.d.ts +3 -3
- package/lib/types/src/domain-errors.d.ts +2 -16
- package/lib/types/src/edit-tool.d.ts +41 -0
- package/lib/types/src/edit-undo.d.ts +2 -11
- package/lib/types/src/file-content/index.d.ts +1 -1
- package/lib/types/src/file-content/preview.d.ts +1 -1
- package/lib/types/src/hash-store.d.ts +2 -2
- package/lib/types/src/hashline/alphabet.d.ts +16 -1
- package/lib/types/src/hashline/hash-identity.d.ts +10 -4
- package/lib/types/src/mutation-engine/engine.d.ts +31 -0
- package/lib/types/src/mutation-engine/pipeline.d.ts +1 -8
- package/lib/types/src/mutation-engine/types.d.ts +50 -40
- package/lib/types/src/payload-contract.d.ts +135 -0
- package/lib/types/src/served-session/serve-render.d.ts +0 -6
- package/lib/types/src/undo-store.d.ts +1 -17
- package/package.json +21 -10
- package/prompts/edit-guidelines.md +10 -0
- package/prompts/edit-snippet.md +1 -0
- package/prompts/edit.md +1 -0
- package/prompts/read-guidelines.md +4 -4
- package/prompts/read.md +1 -1
- package/prompts/undo-last-edit-guidelines.md +2 -2
- package/prompts/undo-last-edit-snippet.md +1 -1
- package/prompts/undo-last-edit.md +1 -1
- package/src/bash-classifier.ts +1626 -0
- package/src/bash-conversion/decide.ts +812 -0
- package/src/bash-display-transform.ts +50 -0
- package/src/bash-multifile-config.ts +239 -0
- package/src/bash-view-config.ts +158 -0
- package/src/cut-repair.ts +11 -77
- package/src/domain-errors.ts +3 -35
- package/src/edit-normalize.ts +8 -0
- package/src/edit-render.ts +161 -0
- package/src/edit-tool.ts +118 -0
- package/src/edit-undo.ts +25 -379
- package/src/edit.ts +136 -0
- package/src/file-content/index.ts +1 -1
- package/src/file-content/preview.ts +1 -1
- package/src/file-content/served-header.ts +1 -1
- package/src/hash-store.ts +1 -18
- package/src/hashline/alphabet.ts +42 -1
- package/src/hashline/hash-identity.ts +12 -3
- package/src/integrations/pi-lens/command.ts +712 -0
- package/src/integrations/pi-lens/config.ts +148 -0
- package/src/integrations/pi-lens/io-bridge-adapter.ts +234 -0
- package/src/integrations/pi-lens/io-bridge.ts +112 -0
- package/src/integrations/pi-lens/settings-overlay.ts +272 -0
- package/src/lifecycle-hooks/index.ts +1992 -0
- package/src/lifecycle-hooks/types.ts +64 -0
- package/src/mutated-files.ts +75 -0
- package/src/mutation-engine/engine.ts +239 -0
- package/src/mutation-engine/index.ts +17 -0
- package/src/mutation-engine/pipeline.ts +27 -198
- package/src/mutation-engine/types.ts +57 -39
- package/src/mutation-queue.ts +1 -1
- package/src/payload-contract.ts +685 -0
- package/src/preview-controller.ts +84 -0
- package/src/prompts.ts +43 -10
- package/src/read.ts +865 -0
- package/src/search-hook.ts +41 -0
- package/src/served-session/serve-render.ts +2 -2
- package/src/store-config.ts +73 -0
- package/src/store-janitor-warning.ts +169 -0
- package/src/store-janitor.ts +539 -0
- package/src/store-kernel/settings.ts +233 -0
- package/src/store.ts +154 -0
- package/src/tui-presenter.ts +134 -0
- package/src/undo-store.ts +5 -74
- package/src/write-hook.ts +130 -0
- package/lib/types/dsh/hide.d.ts +0 -78
- package/lib/types/dsh/tools/apply-patch.d.ts +0 -38
- package/lib/types/src/apply-patch/commit.d.ts +0 -49
- package/lib/types/src/apply-patch/grammar.d.ts +0 -29
- package/lib/types/src/apply-patch/lifecycle.d.ts +0 -32
- package/lib/types/src/apply-patch/plan.d.ts +0 -50
- package/lib/types/src/apply-patch/render.d.ts +0 -51
- package/lib/types/src/apply-patch/resolve.d.ts +0 -129
- package/lib/types/src/apply-patch/tool.d.ts +0 -54
- package/lib/types/src/apply-patch/types.d.ts +0 -362
- package/lib/types/src/fs-lifecycle.d.ts +0 -44
- package/lib/types/src/host-utils.d.ts +0 -63
- package/lib/types/src/mutation-engine/edit-request.d.ts +0 -34
- package/prompts/apply-patch-guidelines.md +0 -10
- package/prompts/apply-patch.md +0 -1
- package/src/apply-patch/commit.ts +0 -190
- package/src/apply-patch/grammar.ts +0 -620
- package/src/apply-patch/index.ts +0 -17
- package/src/apply-patch/lifecycle.ts +0 -195
- package/src/apply-patch/plan.ts +0 -800
- package/src/apply-patch/render.ts +0 -137
- package/src/apply-patch/resolve.ts +0 -470
- package/src/apply-patch/tool.ts +0 -257
- package/src/apply-patch/types.ts +0 -387
- package/src/fs-lifecycle.ts +0 -201
- package/src/host-utils.ts +0 -224
- package/src/mutation-engine/edit-request.ts +0 -39
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
import type { Theme } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import { getPreviewInput as contractPreviewInput, type EditItem } from "./payload-contract.js";
|
|
3
|
+
import type { EditDetails } from "./edit-response.js";
|
|
4
|
+
|
|
5
|
+
export type FgT = Pick<Theme, "fg">;
|
|
6
|
+
export type CallT = Pick<Theme, "fg" | "bold">;
|
|
7
|
+
export type MdTheme = Pick<Theme, "fg" | "bold" | "italic" | "underline" | "strikethrough">;
|
|
8
|
+
|
|
9
|
+
export type RPreview = { diff: string } | { error: string };
|
|
10
|
+
|
|
11
|
+
export type RRState = {
|
|
12
|
+
argsKey?: string;
|
|
13
|
+
preview?: RPreview;
|
|
14
|
+
previewGeneration?: number;
|
|
15
|
+
previewTimer?: ReturnType<typeof setTimeout>;
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
export function getPreviewInput(args: unknown): { file: string; edits: EditItem[] } | null {
|
|
19
|
+
return contractPreviewInput(args);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function colorLines(lines: string[], theme: FgT): string[] {
|
|
23
|
+
return lines.map((line) => {
|
|
24
|
+
if (line.startsWith("+") && !line.startsWith("+++")) {
|
|
25
|
+
return theme.fg("success", line);
|
|
26
|
+
}
|
|
27
|
+
if (line.startsWith("-") && !line.startsWith("---")) {
|
|
28
|
+
return theme.fg("error", line);
|
|
29
|
+
}
|
|
30
|
+
return theme.fg("dim", line);
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function fmtPreview(diff: string, expanded: boolean, theme: FgT): string {
|
|
35
|
+
const lines = diff.split("\n");
|
|
36
|
+
const maxLines = expanded ? 40 : 16;
|
|
37
|
+
const shown = colorLines(lines.slice(0, maxLines), theme);
|
|
38
|
+
|
|
39
|
+
if (lines.length > maxLines) {
|
|
40
|
+
shown.push(theme.fg("muted", `... ${lines.length - maxLines} more diff lines`));
|
|
41
|
+
}
|
|
42
|
+
return shown.join("\n");
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export function fmtResult(diff: string, theme: FgT): string {
|
|
46
|
+
return colorLines(diff.split("\n"), theme).join("\n");
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export function fmtCall(
|
|
50
|
+
args: { file: string; edits: EditItem[] } | null,
|
|
51
|
+
state: RRState,
|
|
52
|
+
expanded: boolean,
|
|
53
|
+
theme: CallT,
|
|
54
|
+
): string {
|
|
55
|
+
const path = args?.file;
|
|
56
|
+
const pathDisplay =
|
|
57
|
+
typeof path === "string" && path.length > 0
|
|
58
|
+
? theme.fg("accent", path)
|
|
59
|
+
: theme.fg("toolOutput", "...");
|
|
60
|
+
const arity = args && args.edits.length > 1 ? ` (${args.edits.length} edits)` : "";
|
|
61
|
+
let text = `${theme.fg("toolTitle", theme.bold("edit"))} ${pathDisplay}${arity}`;
|
|
62
|
+
|
|
63
|
+
if (!state.preview) {
|
|
64
|
+
return text;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
if ("error" in state.preview) {
|
|
68
|
+
text += `\n\n${theme.fg("error", state.preview.error)}`;
|
|
69
|
+
return text;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
if (state.preview.diff) {
|
|
73
|
+
text += `\n\n${fmtPreview(state.preview.diff, expanded, theme)}`;
|
|
74
|
+
}
|
|
75
|
+
return text;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export function getResultText(result: {
|
|
79
|
+
content?: Array<{ type: string; text?: string }>;
|
|
80
|
+
}): string | undefined {
|
|
81
|
+
const textContent = result.content?.find(
|
|
82
|
+
(entry): entry is { type: "text"; text: string } =>
|
|
83
|
+
entry.type === "text" && typeof entry.text === "string",
|
|
84
|
+
);
|
|
85
|
+
return textContent?.text;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export function isApplied(details: EditDetails | undefined): boolean {
|
|
89
|
+
const metrics = details?.metrics;
|
|
90
|
+
return (
|
|
91
|
+
metrics?.classification === "applied" &&
|
|
92
|
+
metrics.added_lines !== undefined &&
|
|
93
|
+
metrics.removed_lines !== undefined
|
|
94
|
+
);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export function buildAppliedText(details: EditDetails | undefined, theme: FgT): string | undefined {
|
|
98
|
+
const sections: string[] = [];
|
|
99
|
+
if (details?.diff) {
|
|
100
|
+
sections.push(fmtResult(details.diff, theme));
|
|
101
|
+
}
|
|
102
|
+
const warnings = details?.warnings;
|
|
103
|
+
if (warnings?.length) {
|
|
104
|
+
sections.push(theme.fg("dim", warnings.join("\n")));
|
|
105
|
+
}
|
|
106
|
+
const driftNotice = details?.driftNotice;
|
|
107
|
+
if (driftNotice) {
|
|
108
|
+
sections.push(theme.fg("dim", driftNotice));
|
|
109
|
+
}
|
|
110
|
+
return sections.length > 0 ? sections.join("\n\n") : undefined;
|
|
111
|
+
}
|
|
112
|
+
function trimEmpty(lines: string[]): string[] {
|
|
113
|
+
let start = 0;
|
|
114
|
+
let end = lines.length;
|
|
115
|
+
|
|
116
|
+
while (start < end && lines[start] === "") {
|
|
117
|
+
start++;
|
|
118
|
+
}
|
|
119
|
+
while (end > start && lines[end - 1] === "") {
|
|
120
|
+
end--;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
return lines.slice(start, end);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
export function fmtResultMd(text: string): string {
|
|
127
|
+
return trimEmpty(text.split("\n")).join("\n");
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
export function mkMdTheme(theme: MdTheme) {
|
|
131
|
+
return {
|
|
132
|
+
heading: (text: string) => theme.fg("mdHeading", text),
|
|
133
|
+
link: (text: string) => theme.fg("mdLink", text),
|
|
134
|
+
linkUrl: (text: string) => theme.fg("mdLinkUrl", text),
|
|
135
|
+
code: (text: string) => theme.fg("mdCode", text),
|
|
136
|
+
codeBlock: (text: string) => theme.fg("mdCodeBlock", text),
|
|
137
|
+
codeBlockBorder: (text: string) => theme.fg("mdCodeBlockBorder", text),
|
|
138
|
+
quote: (text: string) => theme.fg("mdQuote", text),
|
|
139
|
+
quoteBorder: (text: string) => theme.fg("mdQuoteBorder", text),
|
|
140
|
+
hr: (text: string) => theme.fg("mdHr", text),
|
|
141
|
+
listBullet: (text: string) => theme.fg("mdListBullet", text),
|
|
142
|
+
bold: (text: string) => theme.bold(text),
|
|
143
|
+
italic: (text: string) => (theme.italic ? theme.italic(text) : text),
|
|
144
|
+
underline: (text: string) => (theme.underline ? theme.underline(text) : text),
|
|
145
|
+
strikethrough: (text: string) => (theme.strikethrough ? theme.strikethrough(text) : text),
|
|
146
|
+
highlightCode: (code: string, lang?: string) =>
|
|
147
|
+
code.split("\n").map((line) => {
|
|
148
|
+
if (lang === "diff") {
|
|
149
|
+
if (line.startsWith("+") && !line.startsWith("+++")) {
|
|
150
|
+
return theme.fg("toolDiffAdded", line);
|
|
151
|
+
}
|
|
152
|
+
if (line.startsWith("-") && !line.startsWith("---")) {
|
|
153
|
+
return theme.fg("toolDiffRemoved", line);
|
|
154
|
+
}
|
|
155
|
+
return theme.fg("toolDiffContext", line);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
return theme.fg("mdCodeBlock", line);
|
|
159
|
+
}),
|
|
160
|
+
};
|
|
161
|
+
}
|
package/src/edit-tool.ts
ADDED
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
/** SAFETY: EditTool — deep module owning the edit Use Case seam.
|
|
2
|
+
*
|
|
3
|
+
* Graded surface: one narrow interface `EditTool { execute, preview }`
|
|
4
|
+
* hides pipeline delegation, warnings/drift stitching.
|
|
5
|
+
* Frameworks (pi TUI) never cross this seam — that lives in TuiPresenter.
|
|
6
|
+
* Clean Architecture: Use Cases depend inward only (mutation-engine),
|
|
7
|
+
* never outward to Frameworks.
|
|
8
|
+
*
|
|
9
|
+
* Typed boundaries: validate once at admission (payload-contract via normReq/assertReq),
|
|
10
|
+
* trust inside. Immutable state: never mutates caller-owned request.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { constants } from "node:fs";
|
|
14
|
+
import { sessionKeyFor } from "./served-session/session.js";
|
|
15
|
+
import { normReq, assertReq, type NormalizedEditRequest } from "./payload-contract.js";
|
|
16
|
+
import { execute as engineExecute, preview as enginePreview } from "./mutation-engine/engine.js";
|
|
17
|
+
import { isMutationSuccess } from "./mutation-engine/types.js";
|
|
18
|
+
import { attachEnvelope } from "./error-envelope.js";
|
|
19
|
+
|
|
20
|
+
type EditToolContext = {
|
|
21
|
+
cwd: string;
|
|
22
|
+
sessionManager?: { getSessionId(): string };
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
export type PreviewContext = {
|
|
26
|
+
sessionManager?: { getSessionId(): string };
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
type PreviewResult = { diff: string } | { error: string };
|
|
30
|
+
|
|
31
|
+
export interface EditTool {
|
|
32
|
+
/** SAFETY: Execute a validated edit; returns pi tool_result or throws on failure. Admission: params validated by normReq/assertReq. */
|
|
33
|
+
execute(
|
|
34
|
+
params: unknown,
|
|
35
|
+
signal: AbortSignal | undefined,
|
|
36
|
+
ctx: EditToolContext,
|
|
37
|
+
): Promise<{
|
|
38
|
+
content: Array<{ type: "text"; text: string }>;
|
|
39
|
+
details: unknown;
|
|
40
|
+
}>;
|
|
41
|
+
/** SAFETY: Preview without persisting — mirrors pi's compPreview(request, cwd, ctx). The ctx must carry the session whose serves the anchors came from (#165). */
|
|
42
|
+
preview(request: unknown, cwd: string, ctx: PreviewContext): Promise<PreviewResult>;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export function createEditTool(): EditTool {
|
|
46
|
+
return {
|
|
47
|
+
async execute(params, signal, ctx) {
|
|
48
|
+
const canonical = normReq(params);
|
|
49
|
+
assertReq(canonical);
|
|
50
|
+
// SAFETY: ctx is untyped at pi boundary — cast validated by pi's runtime context shape (cwd + sessionManager)
|
|
51
|
+
const sessionKey = sessionKeyFor(
|
|
52
|
+
ctx as unknown as { sessionManager?: { getSessionId(): string } },
|
|
53
|
+
);
|
|
54
|
+
// SAFETY: canonical is validated NormalizedEditRequest after assertReq — trusted inside boundary
|
|
55
|
+
const result = await engineExecute(canonical as NormalizedEditRequest, ctx.cwd, {
|
|
56
|
+
accessMode: constants.R_OK | constants.W_OK,
|
|
57
|
+
// SAFETY: signal is AbortSignal | undefined at pi boundary — runtime check via engine's abortIf
|
|
58
|
+
...(signal !== undefined ? { signal } : {}),
|
|
59
|
+
sessionKey,
|
|
60
|
+
});
|
|
61
|
+
if (isMutationSuccess(result)) {
|
|
62
|
+
// SAFETY: toolResult is validated by isMutationSuccess discriminated union guard
|
|
63
|
+
return result.toolResult as {
|
|
64
|
+
content: Array<{ type: "text"; text: string }>;
|
|
65
|
+
details: unknown;
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
// WHY: every range-family producer emits `details.cause` (user-facing diagnosis) —
|
|
69
|
+
// WHY: carry it on the thrown error so callers catching the message still see the cause.
|
|
70
|
+
// WHY: the whole failure envelope (code, diagnosis, rows, block) is stamped through the
|
|
71
|
+
// WHY: envelope assembler (src/error-envelope.ts) — this seam never hand-copies fields.
|
|
72
|
+
const failure = new Error(result.message);
|
|
73
|
+
attachEnvelope(failure, {
|
|
74
|
+
code: result.code,
|
|
75
|
+
...(result.cause !== undefined ? { cause: result.cause } : {}),
|
|
76
|
+
// WHY: the raw payload message rides the envelope so the adapter can name the tool.
|
|
77
|
+
// WHY: The rendered message alone cannot say which call raised the refusal.
|
|
78
|
+
...(result.payloadMessage !== undefined ? { payloadMessage: result.payloadMessage } : {}),
|
|
79
|
+
servedRows: result.servedRows ?? [],
|
|
80
|
+
servedBlock: result.servedBlock ?? "",
|
|
81
|
+
});
|
|
82
|
+
throw failure;
|
|
83
|
+
},
|
|
84
|
+
async preview(request, cwd, ctx) {
|
|
85
|
+
try {
|
|
86
|
+
const normalized = normReq(request);
|
|
87
|
+
assertReq(normalized);
|
|
88
|
+
// SAFETY: normalized is validated NormalizedEditRequest after assertReq
|
|
89
|
+
// WHY: (#165) preview verifies against the SAME session the anchors were served to —
|
|
90
|
+
// WHY: without a session, sessionKeyFor fails loud here instead of minting a key whose
|
|
91
|
+
// WHY: lease lookups all miss as a misleading E_UNKNOWN_ANCHOR.
|
|
92
|
+
const result = await enginePreview(normalized as NormalizedEditRequest, cwd, {
|
|
93
|
+
accessMode: constants.R_OK,
|
|
94
|
+
sessionKey: sessionKeyFor(ctx),
|
|
95
|
+
});
|
|
96
|
+
if (!isMutationSuccess(result)) {
|
|
97
|
+
return { error: result.message };
|
|
98
|
+
}
|
|
99
|
+
const file = result.raw;
|
|
100
|
+
// WHY: the call's own classification, not the primary file's bytes: a multi-file edit may
|
|
101
|
+
// WHY: change another file while the primary stands still, and that IS a change to preview.
|
|
102
|
+
if (result.metrics.classification === "noop") {
|
|
103
|
+
return {
|
|
104
|
+
error: `No changes made to ${file.path}. The edit produced identical content.`,
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
return {
|
|
108
|
+
// WHY: #174 single projection — the engine's `preview` seam already produced the
|
|
109
|
+
// WHY: `genDiff` text with the pane's context; this display path renders it and
|
|
110
|
+
// WHY: never re-projects.
|
|
111
|
+
diff: result.diff,
|
|
112
|
+
};
|
|
113
|
+
} catch (error: unknown) {
|
|
114
|
+
return { error: error instanceof Error ? error.message : String(error) };
|
|
115
|
+
}
|
|
116
|
+
},
|
|
117
|
+
};
|
|
118
|
+
}
|