@agimon-ai/doompi-edit 0.0.1-alpha.2 → 0.0.1-alpha.20
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 +1 -1
- package/package.json +33 -12
- package/src/exports/webClient.ts +1 -0
- package/src/web/EditToolMessage.tsx +90 -0
- package/src/web/editToolView.ts +75 -0
- package/src/web/index.ts +11 -0
- package/src/web/tsconfig.json +23 -0
package/README.md
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@agimon-ai/doompi-edit",
|
|
3
|
-
"version": "0.0.1-alpha.
|
|
3
|
+
"version": "0.0.1-alpha.20",
|
|
4
4
|
"description": "Snapshot-bound hashline edit tool for Pi and DoomPi.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"ai",
|
|
@@ -13,10 +13,20 @@
|
|
|
13
13
|
"pi-package"
|
|
14
14
|
],
|
|
15
15
|
"homepage": "https://agimon.ai",
|
|
16
|
+
"bugs": {
|
|
17
|
+
"url": "https://github.com/AgiFlow/doompi/issues"
|
|
18
|
+
},
|
|
16
19
|
"license": "MIT",
|
|
17
20
|
"author": "Vuong Ngo",
|
|
21
|
+
"repository": {
|
|
22
|
+
"type": "git",
|
|
23
|
+
"url": "git+https://github.com/AgiFlow/doompi.git",
|
|
24
|
+
"directory": "packages/default/doompi-edit"
|
|
25
|
+
},
|
|
18
26
|
"files": [
|
|
19
|
-
"dist"
|
|
27
|
+
"dist",
|
|
28
|
+
"src/web",
|
|
29
|
+
"src/exports/webClient.ts"
|
|
20
30
|
],
|
|
21
31
|
"type": "module",
|
|
22
32
|
"main": "./dist/index.cjs",
|
|
@@ -40,23 +50,29 @@
|
|
|
40
50
|
},
|
|
41
51
|
"dependencies": {
|
|
42
52
|
"@deepseek-ai/cordis": "4.0.1",
|
|
43
|
-
"typebox": "1.3.
|
|
44
|
-
"@agimon-ai/doompi-extension-contracts": "0.0.1-alpha.
|
|
45
|
-
"@agimon-ai/doompi-ui": "0.0.1-alpha.
|
|
46
|
-
"@agimon-ai/doompi-
|
|
53
|
+
"typebox": "1.3.19",
|
|
54
|
+
"@agimon-ai/doompi-extension-contracts": "0.0.1-alpha.47",
|
|
55
|
+
"@agimon-ai/doompi-ui": "0.0.1-alpha.48",
|
|
56
|
+
"@agimon-ai/doompi-web-contracts": "0.0.1-alpha.11",
|
|
57
|
+
"@agimon-ai/doompi-web-components": "0.0.1-alpha.8",
|
|
58
|
+
"@agimon-ai/doompi-hashline": "0.0.1-alpha.19"
|
|
47
59
|
},
|
|
48
60
|
"devDependencies": {
|
|
49
|
-
"@earendil-works/pi-coding-agent": "0.84.
|
|
50
|
-
"@earendil-works/pi-tui": "0.84.
|
|
51
|
-
"@
|
|
61
|
+
"@earendil-works/pi-coding-agent": "0.84.4",
|
|
62
|
+
"@earendil-works/pi-tui": "0.84.4",
|
|
63
|
+
"@tanstack/react-store": "0.11.1",
|
|
64
|
+
"@tanstack/store": "0.11.1",
|
|
65
|
+
"@types/node": "26.4.0",
|
|
66
|
+
"@types/react": "19.2.18",
|
|
52
67
|
"@vitest/coverage-v8": "4.1.11",
|
|
68
|
+
"react": "19.2.8",
|
|
53
69
|
"tsdown": "0.22.14",
|
|
54
70
|
"typescript": "7.0.2",
|
|
55
71
|
"vitest": "4.1.11"
|
|
56
72
|
},
|
|
57
73
|
"peerDependencies": {
|
|
58
|
-
"@earendil-works/pi-coding-agent": "0.84.
|
|
59
|
-
"@earendil-works/pi-tui": "0.84.
|
|
74
|
+
"@earendil-works/pi-coding-agent": "0.84.4",
|
|
75
|
+
"@earendil-works/pi-tui": "0.84.4"
|
|
60
76
|
},
|
|
61
77
|
"peerDependenciesMeta": {
|
|
62
78
|
"@earendil-works/pi-coding-agent": {
|
|
@@ -69,6 +85,11 @@
|
|
|
69
85
|
"engines": {
|
|
70
86
|
"node": ">=22.19.0"
|
|
71
87
|
},
|
|
88
|
+
"doompiWeb": {
|
|
89
|
+
"pluginId": "edit",
|
|
90
|
+
"channels": [],
|
|
91
|
+
"client": "./src/exports/webClient.ts"
|
|
92
|
+
},
|
|
72
93
|
"pi": {
|
|
73
94
|
"extensions": [
|
|
74
95
|
"./dist/extensions/pi.mjs"
|
|
@@ -77,7 +98,7 @@
|
|
|
77
98
|
"scripts": {
|
|
78
99
|
"build": "tsdown",
|
|
79
100
|
"test": "vitest --run",
|
|
80
|
-
"typecheck": "tsc --noEmit",
|
|
101
|
+
"typecheck": "tsc --noEmit && tsc --noEmit -p src/web/tsconfig.json",
|
|
81
102
|
"lint": "oxlint . && oxfmt . --check",
|
|
82
103
|
"fixcode": "oxlint . --fix && oxfmt ."
|
|
83
104
|
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { webPlugin } from '../web/index.ts';
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import {
|
|
2
|
+
collapseLines,
|
|
3
|
+
MessageItem,
|
|
4
|
+
MessageItemBody,
|
|
5
|
+
MessageItemHeader,
|
|
6
|
+
MessageItemStatus,
|
|
7
|
+
toolTone,
|
|
8
|
+
} from '@agimon-ai/doompi-web-components';
|
|
9
|
+
import type { ToolMessageRenderProps } from '@agimon-ai/doompi-web-contracts';
|
|
10
|
+
import { type DiffRow, editCallView, editResultView, resultTextLines } from './editToolView.ts';
|
|
11
|
+
|
|
12
|
+
/** Diff rows shown until the item expands; about what the old scroll box held. */
|
|
13
|
+
const COLLAPSED_ROWS = 24;
|
|
14
|
+
|
|
15
|
+
// Doom carries a diff on the background the way Magit does: a green wash on
|
|
16
|
+
// added rows, a red wash on removed ones, and the foreground left to the text.
|
|
17
|
+
const ROW_TONE: Readonly<Record<DiffRow['marker'], string>> = {
|
|
18
|
+
'+': 'bg-doom-tint-green text-doom-green',
|
|
19
|
+
'-': 'bg-doom-tint-red text-doom-red',
|
|
20
|
+
' ': 'text-doom-dim',
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* The edit tool's timeline item: `path · N ranges` in the header; the display
|
|
25
|
+
* diff from the result's details, banded by row, in the body. A successful
|
|
26
|
+
* edit whose details never arrived shows nothing, and a failed one shows the
|
|
27
|
+
* tool's message in red.
|
|
28
|
+
*/
|
|
29
|
+
export function EditToolMessage({ args, result, output, running, isError }: ToolMessageRenderProps) {
|
|
30
|
+
const call = editCallView(args);
|
|
31
|
+
const view = result === null || isError ? undefined : editResultView(result);
|
|
32
|
+
return (
|
|
33
|
+
<MessageItem
|
|
34
|
+
tone={toolTone({ running, isError })}
|
|
35
|
+
expandable={view !== undefined && view.rows.length > COLLAPSED_ROWS}
|
|
36
|
+
>
|
|
37
|
+
{({ expanded }) => (
|
|
38
|
+
<>
|
|
39
|
+
<MessageItemHeader title="edit">
|
|
40
|
+
<span data-testid="tool-call-edit" className="flex min-w-0 flex-1 items-center gap-2">
|
|
41
|
+
<span className="truncate text-doom-text">{call.path}</span>
|
|
42
|
+
<span className="shrink-0 text-doom-faint">· {call.ranges}</span>
|
|
43
|
+
</span>
|
|
44
|
+
</MessageItemHeader>
|
|
45
|
+
{isError ? (
|
|
46
|
+
<MessageItemBody data-testid="tool-result-edit" className="flex flex-col">
|
|
47
|
+
<MessageItemStatus tone="error">failed</MessageItemStatus>
|
|
48
|
+
{resultTextLines(result, output).map((line, index) => (
|
|
49
|
+
<span key={`${String(index)}-${line}`} className="whitespace-pre-wrap break-words text-doom-red">
|
|
50
|
+
{line}
|
|
51
|
+
</span>
|
|
52
|
+
))}
|
|
53
|
+
</MessageItemBody>
|
|
54
|
+
) : view === undefined ? (
|
|
55
|
+
running ? (
|
|
56
|
+
<MessageItemBody data-testid="tool-result-edit">
|
|
57
|
+
<MessageItemStatus tone="running">running</MessageItemStatus>
|
|
58
|
+
</MessageItemBody>
|
|
59
|
+
) : null
|
|
60
|
+
) : (
|
|
61
|
+
(() => {
|
|
62
|
+
const { shown, hidden } = collapseLines(view.rows, COLLAPSED_ROWS, expanded);
|
|
63
|
+
return (
|
|
64
|
+
<MessageItemBody data-testid="tool-result-edit" className="flex flex-col font-mono">
|
|
65
|
+
{shown.map((row, index) => (
|
|
66
|
+
<span key={`${String(index)}-${row.lineNumber}`} className={`flex gap-2 ${ROW_TONE[row.marker]}`}>
|
|
67
|
+
<span
|
|
68
|
+
className="shrink-0 text-right text-doom-faint"
|
|
69
|
+
style={{ width: `${String(view.gutter)}ch` }}
|
|
70
|
+
>
|
|
71
|
+
{row.lineNumber}
|
|
72
|
+
</span>
|
|
73
|
+
<span className="w-2 shrink-0">{row.marker === ' ' ? '' : row.marker}</span>
|
|
74
|
+
<span className="min-w-0 whitespace-pre-wrap break-words">{row.content}</span>
|
|
75
|
+
</span>
|
|
76
|
+
))}
|
|
77
|
+
{hidden > 0 ? (
|
|
78
|
+
<MessageItemStatus expands className="pt-1">
|
|
79
|
+
{hidden} more rows
|
|
80
|
+
</MessageItemStatus>
|
|
81
|
+
) : null}
|
|
82
|
+
</MessageItemBody>
|
|
83
|
+
);
|
|
84
|
+
})()
|
|
85
|
+
)}
|
|
86
|
+
</>
|
|
87
|
+
)}
|
|
88
|
+
</MessageItem>
|
|
89
|
+
);
|
|
90
|
+
}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import type { ToolResultView } from '@agimon-ai/doompi-web-contracts';
|
|
2
|
+
|
|
3
|
+
/** Pi emits `+12 text`, `-12 text`, ` 12 text`, and a blank-numbered ` ... ` elision between hunks. */
|
|
4
|
+
const DIFF_ROW_PATTERN = /^([+\- ])( *\d*) (.*)$/u;
|
|
5
|
+
const CONTEXT_MARKER = ' ';
|
|
6
|
+
|
|
7
|
+
export type DiffMarker = '+' | '-' | typeof CONTEXT_MARKER;
|
|
8
|
+
|
|
9
|
+
export interface DiffRow {
|
|
10
|
+
readonly marker: DiffMarker;
|
|
11
|
+
readonly lineNumber: string;
|
|
12
|
+
readonly content: string;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export interface EditCallView {
|
|
16
|
+
readonly path: string;
|
|
17
|
+
/** `N range` or `N ranges`, the count the TUI's call heading shows. */
|
|
18
|
+
readonly ranges: string;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export interface EditResultView {
|
|
22
|
+
readonly rows: readonly DiffRow[];
|
|
23
|
+
/** Widest line number among the rows, in characters, sizing the gutter. */
|
|
24
|
+
readonly gutter: number;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** What the edit call heading shows: the path and how many ranges the call touches. */
|
|
28
|
+
export function editCallView(args: Readonly<Record<string, unknown>>): EditCallView {
|
|
29
|
+
const count = Array.isArray(args.edits) ? args.edits.length : 0;
|
|
30
|
+
return {
|
|
31
|
+
path: typeof args.path === 'string' ? args.path : '',
|
|
32
|
+
ranges: `${count} ${count === 1 ? 'range' : 'ranges'}`,
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function toDiffMarker(value: string | undefined): DiffMarker {
|
|
37
|
+
return value === '+' || value === '-' ? value : CONTEXT_MARKER;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** Pi's display diff as rows; a line that is not a diff row reads as unnumbered context. */
|
|
41
|
+
export function parseDiffRows(diff: string): DiffRow[] {
|
|
42
|
+
return diff.split('\n').map((line) => {
|
|
43
|
+
const expanded = line.replaceAll('\t', ' ');
|
|
44
|
+
const match = DIFF_ROW_PATTERN.exec(expanded);
|
|
45
|
+
if (!match) return { marker: CONTEXT_MARKER, lineNumber: '', content: expanded };
|
|
46
|
+
return { marker: toDiffMarker(match[1]), lineNumber: (match[2] ?? '').trim(), content: match[3] ?? '' };
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** The diff a successful edit carries in its details; undefined when the details never arrived. */
|
|
51
|
+
export function editResultView(result: ToolResultView | null): EditResultView | undefined {
|
|
52
|
+
const details = result?.details;
|
|
53
|
+
if (typeof details !== 'object' || details === null) return undefined;
|
|
54
|
+
const diff = (details as { diff?: unknown }).diff;
|
|
55
|
+
if (typeof diff !== 'string' || diff.length === 0) return undefined;
|
|
56
|
+
const rows = parseDiffRows(diff);
|
|
57
|
+
return { rows, gutter: rows.reduce((widest, row) => Math.max(widest, row.lineNumber.length), 1) };
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/** The result's text lines, for the error case where the diff never happened. */
|
|
61
|
+
export function resultTextLines(result: ToolResultView | null, output: string): string[] {
|
|
62
|
+
const text =
|
|
63
|
+
result === null
|
|
64
|
+
? output
|
|
65
|
+
: result.content
|
|
66
|
+
.flatMap((block) => {
|
|
67
|
+
const record =
|
|
68
|
+
typeof block === 'object' && block !== null ? (block as { type?: unknown; text?: unknown }) : {};
|
|
69
|
+
return record.type === 'text' && typeof record.text === 'string' ? [record.text] : [];
|
|
70
|
+
})
|
|
71
|
+
.join('\n');
|
|
72
|
+
const lines = text.replaceAll('\t', ' ').replaceAll('\r\n', '\n').split('\n');
|
|
73
|
+
while (lines.at(-1) === '') lines.pop();
|
|
74
|
+
return lines;
|
|
75
|
+
}
|
package/src/web/index.ts
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { defineWebPlugin } from '@agimon-ai/doompi-web-contracts';
|
|
2
|
+
import { EditToolMessage } from './EditToolMessage.tsx';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* This package's cockpit presence: the edit tool's timeline card, the web
|
|
6
|
+
* half of the TUI's renderCall and renderResult for hashline edits.
|
|
7
|
+
*/
|
|
8
|
+
export const webPlugin = defineWebPlugin({
|
|
9
|
+
id: 'edit',
|
|
10
|
+
toolRenderers: [{ tools: ['edit'], message: EditToolMessage }],
|
|
11
|
+
});
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2022",
|
|
4
|
+
"lib": ["ES2023", "DOM", "DOM.Iterable"],
|
|
5
|
+
"module": "ESNext",
|
|
6
|
+
"moduleResolution": "Bundler",
|
|
7
|
+
"jsx": "react-jsx",
|
|
8
|
+
"strict": true,
|
|
9
|
+
"noImplicitAny": true,
|
|
10
|
+
"noUnusedLocals": true,
|
|
11
|
+
"noUnusedParameters": true,
|
|
12
|
+
"noImplicitReturns": true,
|
|
13
|
+
"noFallthroughCasesInSwitch": true,
|
|
14
|
+
"noEmit": true,
|
|
15
|
+
"resolveJsonModule": true,
|
|
16
|
+
"esModuleInterop": true,
|
|
17
|
+
"skipLibCheck": true,
|
|
18
|
+
"allowImportingTsExtensions": true,
|
|
19
|
+
"forceConsistentCasingInFileNames": true,
|
|
20
|
+
"types": ["node"]
|
|
21
|
+
},
|
|
22
|
+
"include": [".", "../types", "../exports/webClient.ts"]
|
|
23
|
+
}
|