agent-sh 0.8.0 → 0.10.0
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 +27 -43
- package/dist/agent/agent-loop.d.ts +69 -6
- package/dist/agent/agent-loop.js +954 -153
- package/dist/agent/conversation-state.d.ts +74 -21
- package/dist/agent/conversation-state.js +361 -150
- package/dist/agent/history-file.d.ts +13 -4
- package/dist/agent/history-file.js +110 -36
- package/dist/agent/nuclear-form.d.ts +28 -3
- package/dist/agent/nuclear-form.js +88 -6
- package/dist/agent/skills.d.ts +2 -4
- package/dist/agent/skills.js +10 -4
- package/dist/agent/subagent.d.ts +23 -0
- package/dist/agent/subagent.js +53 -11
- package/dist/agent/system-prompt.d.ts +37 -5
- package/dist/agent/system-prompt.js +100 -67
- package/dist/{token-budget.d.ts → agent/token-budget.d.ts} +5 -4
- package/dist/{token-budget.js → agent/token-budget.js} +15 -20
- package/dist/agent/tool-protocol.d.ts +105 -0
- package/dist/agent/tool-protocol.js +551 -0
- package/dist/agent/tools/bash.js +3 -3
- package/dist/agent/tools/edit-file.js +9 -6
- package/dist/agent/tools/glob.js +4 -2
- package/dist/agent/tools/grep.js +27 -3
- package/dist/agent/tools/ls.js +5 -6
- package/dist/agent/types.d.ts +22 -2
- package/dist/context-manager.d.ts +17 -0
- package/dist/context-manager.js +37 -4
- package/dist/core.d.ts +7 -7
- package/dist/core.js +99 -196
- package/dist/event-bus.d.ts +85 -2
- package/dist/event-bus.js +20 -1
- package/dist/executor.d.ts +4 -3
- package/dist/executor.js +18 -15
- package/dist/extension-loader.d.ts +5 -0
- package/dist/extension-loader.js +143 -19
- package/dist/extensions/agent-backend.d.ts +14 -0
- package/dist/extensions/agent-backend.js +188 -0
- package/dist/extensions/command-suggest.d.ts +3 -3
- package/dist/extensions/command-suggest.js +4 -3
- package/dist/extensions/index.d.ts +19 -0
- package/dist/extensions/index.js +24 -0
- package/dist/extensions/slash-commands.d.ts +1 -1
- package/dist/extensions/slash-commands.js +30 -10
- package/dist/extensions/tui-renderer.js +117 -113
- package/dist/index.js +39 -26
- package/dist/settings.d.ts +40 -3
- package/dist/settings.js +57 -10
- package/dist/{input-handler.d.ts → shell/input-handler.d.ts} +3 -2
- package/dist/{input-handler.js → shell/input-handler.js} +111 -85
- package/dist/{output-parser.d.ts → shell/output-parser.d.ts} +1 -1
- package/dist/{output-parser.js → shell/output-parser.js} +1 -1
- package/dist/{shell.d.ts → shell/shell.d.ts} +8 -2
- package/dist/{shell.js → shell/shell.js} +39 -8
- package/dist/types.d.ts +61 -10
- package/dist/utils/ansi.d.ts +5 -0
- package/dist/utils/ansi.js +1 -1
- package/dist/utils/compositor.d.ts +67 -0
- package/dist/utils/compositor.js +116 -0
- package/dist/utils/diff-renderer.d.ts +9 -0
- package/dist/utils/diff-renderer.js +312 -146
- package/dist/utils/diff.d.ts +21 -2
- package/dist/utils/diff.js +165 -89
- package/dist/utils/floating-panel.d.ts +2 -0
- package/dist/utils/floating-panel.js +30 -14
- package/dist/utils/handler-registry.d.ts +31 -10
- package/dist/utils/handler-registry.js +58 -16
- package/dist/utils/line-editor.d.ts +33 -3
- package/dist/utils/line-editor.js +221 -44
- package/dist/utils/markdown.d.ts +1 -0
- package/dist/utils/markdown.js +1 -1
- package/dist/utils/message-utils.d.ts +35 -0
- package/dist/utils/message-utils.js +75 -0
- package/dist/utils/terminal-buffer.d.ts +5 -1
- package/dist/utils/terminal-buffer.js +18 -2
- package/dist/utils/tool-display.d.ts +1 -1
- package/dist/utils/tool-display.js +4 -4
- package/dist/utils/tool-interactive.d.ts +12 -0
- package/dist/utils/tool-interactive.js +53 -0
- package/examples/extensions/ash-acp-bridge/README.md +39 -0
- package/examples/extensions/ash-acp-bridge/package.json +23 -0
- package/examples/extensions/ash-acp-bridge/src/index.ts +574 -0
- package/examples/extensions/ash-acp-bridge/tsconfig.json +14 -0
- package/examples/extensions/ash-mcp-bridge/README.md +72 -0
- package/examples/extensions/ash-mcp-bridge/index.ts +164 -0
- package/examples/extensions/ash-mcp-bridge/package.json +9 -0
- package/examples/extensions/claude-code-bridge/index.ts +198 -51
- package/examples/extensions/claude-code-bridge/package.json +1 -0
- package/examples/extensions/interactive-prompts.ts +98 -112
- package/examples/extensions/overlay-agent.ts +84 -38
- package/examples/extensions/peer-mesh.ts +565 -0
- package/examples/extensions/pi-bridge/index.ts +2 -2
- package/examples/extensions/questionnaire.ts +260 -0
- package/examples/extensions/subagents.ts +19 -4
- package/examples/extensions/terminal-buffer.ts +32 -53
- package/examples/extensions/tmux-pane.ts +307 -0
- package/examples/extensions/user-shell.ts +136 -0
- package/examples/extensions/web-access.ts +335 -0
- package/package.json +44 -2
- package/dist/agent/tools/display.d.ts +0 -13
- package/dist/agent/tools/display.js +0 -70
- package/dist/agent/tools/user-shell.d.ts +0 -13
- package/dist/agent/tools/user-shell.js +0 -87
- package/dist/extensions/overlay-agent.d.ts +0 -14
- package/dist/extensions/overlay-agent.js +0 -147
- package/dist/extensions/terminal-buffer.d.ts +0 -14
- package/dist/extensions/terminal-buffer.js +0 -125
package/dist/utils/diff.js
CHANGED
|
@@ -1,9 +1,85 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* Line-level diff computation, powered by the `diff` npm package.
|
|
3
|
+
*
|
|
4
|
+
* Exposes a unified `DiffResult` interface consumed by the diff renderer.
|
|
5
|
+
* Three entry points cover the main use cases:
|
|
6
|
+
*
|
|
7
|
+
* computeDiff — full-file diff (write_file, or when edit region can't be located)
|
|
8
|
+
* computeEditDiff — edit_file: locates the edit region, builds the new file, full diff
|
|
9
|
+
* computeInputDiff — fast preview: diffs only old_text vs new_text, no file I/O
|
|
3
10
|
*/
|
|
11
|
+
import * as Diff from "diff";
|
|
12
|
+
// ── Helpers ──────────────────────────────────────────────────────────
|
|
13
|
+
/**
|
|
14
|
+
* Convert a `diff` library Change[] into our DiffLine[], tracking real
|
|
15
|
+
* old/new line numbers.
|
|
16
|
+
*/
|
|
17
|
+
function changesToDiffLines(changes) {
|
|
18
|
+
const result = [];
|
|
19
|
+
let oldNo = 0;
|
|
20
|
+
let newNo = 0;
|
|
21
|
+
for (const change of changes) {
|
|
22
|
+
const lines = change.value.replace(/\n$/, "").split("\n");
|
|
23
|
+
for (const text of lines) {
|
|
24
|
+
if (change.added) {
|
|
25
|
+
newNo++;
|
|
26
|
+
result.push({ type: "added", oldNo: null, newNo, text });
|
|
27
|
+
}
|
|
28
|
+
else if (change.removed) {
|
|
29
|
+
oldNo++;
|
|
30
|
+
result.push({ type: "removed", oldNo, newNo: null, text });
|
|
31
|
+
}
|
|
32
|
+
else {
|
|
33
|
+
oldNo++;
|
|
34
|
+
newNo++;
|
|
35
|
+
result.push({ type: "context", oldNo, newNo, text });
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
return result;
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Group raw DiffLines into hunks with `context` lines of surrounding context.
|
|
43
|
+
*/
|
|
44
|
+
function groupHunks(lines, ctx) {
|
|
45
|
+
const changeIdx = [];
|
|
46
|
+
for (let i = 0; i < lines.length; i++)
|
|
47
|
+
if (lines[i].type !== "context")
|
|
48
|
+
changeIdx.push(i);
|
|
49
|
+
if (changeIdx.length === 0)
|
|
50
|
+
return [];
|
|
51
|
+
const hunks = [];
|
|
52
|
+
let start = Math.max(0, changeIdx[0] - ctx);
|
|
53
|
+
let end = Math.min(lines.length - 1, changeIdx[0] + ctx);
|
|
54
|
+
for (let k = 1; k < changeIdx.length; k++) {
|
|
55
|
+
const ns = Math.max(0, changeIdx[k] - ctx);
|
|
56
|
+
const ne = Math.min(lines.length - 1, changeIdx[k] + ctx);
|
|
57
|
+
if (ns <= end + 1) {
|
|
58
|
+
end = ne;
|
|
59
|
+
}
|
|
60
|
+
else {
|
|
61
|
+
hunks.push({ lines: lines.slice(start, end + 1) });
|
|
62
|
+
start = ns;
|
|
63
|
+
end = ne;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
hunks.push({ lines: lines.slice(start, end + 1) });
|
|
67
|
+
return hunks;
|
|
68
|
+
}
|
|
69
|
+
function countChanges(lines) {
|
|
70
|
+
let added = 0;
|
|
71
|
+
let removed = 0;
|
|
72
|
+
for (const l of lines) {
|
|
73
|
+
if (l.type === "added")
|
|
74
|
+
added++;
|
|
75
|
+
else if (l.type === "removed")
|
|
76
|
+
removed++;
|
|
77
|
+
}
|
|
78
|
+
return { added, removed };
|
|
79
|
+
}
|
|
80
|
+
// ── Public API ───────────────────────────────────────────────────────
|
|
4
81
|
/**
|
|
5
82
|
* Compute a line-level diff between old and new file content.
|
|
6
|
-
* Returns grouped hunks with 3 lines of context around each change.
|
|
7
83
|
*/
|
|
8
84
|
export function computeDiff(oldText, newText) {
|
|
9
85
|
// New file — everything is an addition
|
|
@@ -28,37 +104,11 @@ export function computeDiff(oldText, newText) {
|
|
|
28
104
|
}
|
|
29
105
|
// Identical — nothing to show
|
|
30
106
|
if (oldText === newText) {
|
|
31
|
-
return {
|
|
32
|
-
hunks: [],
|
|
33
|
-
added: 0,
|
|
34
|
-
removed: 0,
|
|
35
|
-
isIdentical: true,
|
|
36
|
-
isNewFile: false,
|
|
37
|
-
};
|
|
38
|
-
}
|
|
39
|
-
// Build LCS table and backtrack to produce diff lines
|
|
40
|
-
const a = oldText.split("\n");
|
|
41
|
-
const b = newText.split("\n");
|
|
42
|
-
// Bail out if LCS table would be too large (avoids OOM / hang)
|
|
43
|
-
if (a.length * b.length > 10_000_000) {
|
|
44
|
-
return {
|
|
45
|
-
hunks: [],
|
|
46
|
-
added: b.length,
|
|
47
|
-
removed: a.length,
|
|
48
|
-
isIdentical: false,
|
|
49
|
-
isNewFile: false,
|
|
50
|
-
};
|
|
51
|
-
}
|
|
52
|
-
const dp = buildLcs(a, b);
|
|
53
|
-
const raw = backtrack(dp, a, b);
|
|
54
|
-
let added = 0;
|
|
55
|
-
let removed = 0;
|
|
56
|
-
for (const l of raw) {
|
|
57
|
-
if (l.type === "added")
|
|
58
|
-
added++;
|
|
59
|
-
else if (l.type === "removed")
|
|
60
|
-
removed++;
|
|
107
|
+
return { hunks: [], added: 0, removed: 0, isIdentical: true, isNewFile: false };
|
|
61
108
|
}
|
|
109
|
+
const changes = Diff.diffLines(oldText, newText);
|
|
110
|
+
const raw = changesToDiffLines(changes);
|
|
111
|
+
const { added, removed } = countChanges(raw);
|
|
62
112
|
return {
|
|
63
113
|
hunks: groupHunks(raw, 3),
|
|
64
114
|
added,
|
|
@@ -67,66 +117,92 @@ export function computeDiff(oldText, newText) {
|
|
|
67
117
|
isNewFile: false,
|
|
68
118
|
};
|
|
69
119
|
}
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
type: "removed",
|
|
99
|
-
oldNo: i,
|
|
100
|
-
newNo: null,
|
|
101
|
-
text: a[i - 1],
|
|
102
|
-
});
|
|
103
|
-
i--;
|
|
120
|
+
/**
|
|
121
|
+
* Compute a diff for an edit operation where we know the old/new text.
|
|
122
|
+
* Locates the edit region(s) in the file, constructs the full new file,
|
|
123
|
+
* then diffs the whole thing so line numbers are file-relative.
|
|
124
|
+
*/
|
|
125
|
+
export function computeEditDiff(oldFileText, editOld, editNew, replaceAll = false) {
|
|
126
|
+
const a = oldFileText.split("\n");
|
|
127
|
+
const editOldLines = editOld.split("\n");
|
|
128
|
+
const editNewLines = editNew.split("\n");
|
|
129
|
+
// Find all occurrences of editOld in the file
|
|
130
|
+
const regions = [];
|
|
131
|
+
if (replaceAll) {
|
|
132
|
+
let i = 0;
|
|
133
|
+
while (i <= a.length - editOldLines.length) {
|
|
134
|
+
let match = true;
|
|
135
|
+
for (let k = 0; k < editOldLines.length; k++) {
|
|
136
|
+
if (a[i + k] !== editOldLines[k]) {
|
|
137
|
+
match = false;
|
|
138
|
+
break;
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
if (match) {
|
|
142
|
+
regions.push({ start: i, end: i + editOldLines.length });
|
|
143
|
+
i += editOldLines.length;
|
|
144
|
+
}
|
|
145
|
+
else {
|
|
146
|
+
i++;
|
|
147
|
+
}
|
|
104
148
|
}
|
|
105
149
|
}
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
const ns = Math.max(0, changeIdx[k] - ctx);
|
|
120
|
-
const ne = Math.min(lines.length - 1, changeIdx[k] + ctx);
|
|
121
|
-
if (ns <= end + 1) {
|
|
122
|
-
end = ne;
|
|
150
|
+
else {
|
|
151
|
+
for (let i = 0; i <= a.length - editOldLines.length; i++) {
|
|
152
|
+
let match = true;
|
|
153
|
+
for (let k = 0; k < editOldLines.length; k++) {
|
|
154
|
+
if (a[i + k] !== editOldLines[k]) {
|
|
155
|
+
match = false;
|
|
156
|
+
break;
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
if (match) {
|
|
160
|
+
regions.push({ start: i, end: i + editOldLines.length });
|
|
161
|
+
break;
|
|
162
|
+
}
|
|
123
163
|
}
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
164
|
+
}
|
|
165
|
+
// Build the full new file
|
|
166
|
+
let newFile;
|
|
167
|
+
if (replaceAll && regions.length > 0) {
|
|
168
|
+
const parts = [];
|
|
169
|
+
let last = 0;
|
|
170
|
+
for (const r of regions) {
|
|
171
|
+
parts.push(...a.slice(last, r.start));
|
|
172
|
+
parts.push(...editNewLines);
|
|
173
|
+
last = r.end;
|
|
128
174
|
}
|
|
175
|
+
parts.push(...a.slice(last));
|
|
176
|
+
newFile = parts;
|
|
129
177
|
}
|
|
130
|
-
|
|
131
|
-
|
|
178
|
+
else if (regions.length === 1) {
|
|
179
|
+
const r = regions[0];
|
|
180
|
+
newFile = [...a.slice(0, r.start), ...editNewLines, ...a.slice(r.end)];
|
|
181
|
+
}
|
|
182
|
+
else {
|
|
183
|
+
// Couldn't locate edit — fall back to string replace + full diff
|
|
184
|
+
const newContent = replaceAll
|
|
185
|
+
? oldFileText.split(editOld).join(editNew)
|
|
186
|
+
: oldFileText.replace(editOld, editNew);
|
|
187
|
+
return computeDiff(oldFileText, newContent);
|
|
188
|
+
}
|
|
189
|
+
return computeDiff(oldFileText, newFile.join("\n"));
|
|
190
|
+
}
|
|
191
|
+
/**
|
|
192
|
+
* Diff two edit strings directly — no file read needed.
|
|
193
|
+
* Line numbers are relative to the edit region, not the file.
|
|
194
|
+
* Use for permission prompt previews where speed matters more than
|
|
195
|
+
* exact file-relative line numbers.
|
|
196
|
+
*/
|
|
197
|
+
export function computeInputDiff(oldText, newText) {
|
|
198
|
+
const changes = Diff.diffLines(oldText, newText);
|
|
199
|
+
const raw = changesToDiffLines(changes);
|
|
200
|
+
const { added, removed } = countChanges(raw);
|
|
201
|
+
return {
|
|
202
|
+
hunks: groupHunks(raw, 3),
|
|
203
|
+
added,
|
|
204
|
+
removed,
|
|
205
|
+
isIdentical: false,
|
|
206
|
+
isNewFile: false,
|
|
207
|
+
};
|
|
132
208
|
}
|
|
@@ -182,6 +182,8 @@ export declare class FloatingPanel {
|
|
|
182
182
|
private ensureBuffer;
|
|
183
183
|
/** Whether the panel has an active conversation (may be hidden). */
|
|
184
184
|
get active(): boolean;
|
|
185
|
+
/** Whether the agent is currently processing a query. */
|
|
186
|
+
get processing(): boolean;
|
|
185
187
|
/** Whether the panel is currently visible on screen. */
|
|
186
188
|
get visible(): boolean;
|
|
187
189
|
get terminalBuffer(): TerminalBuffer | null;
|
|
@@ -328,6 +328,10 @@ export class FloatingPanel {
|
|
|
328
328
|
get active() {
|
|
329
329
|
return this.phase !== "idle";
|
|
330
330
|
}
|
|
331
|
+
/** Whether the agent is currently processing a query. */
|
|
332
|
+
get processing() {
|
|
333
|
+
return this.phase === "active";
|
|
334
|
+
}
|
|
331
335
|
/** Whether the panel is currently visible on screen. */
|
|
332
336
|
get visible() {
|
|
333
337
|
return this._visible;
|
|
@@ -515,7 +519,7 @@ export class FloatingPanel {
|
|
|
515
519
|
this.render();
|
|
516
520
|
}
|
|
517
521
|
getInput() {
|
|
518
|
-
return this.editor.
|
|
522
|
+
return this.editor.text;
|
|
519
523
|
}
|
|
520
524
|
requestRender() {
|
|
521
525
|
this.scheduleRender();
|
|
@@ -634,7 +638,7 @@ export class FloatingPanel {
|
|
|
634
638
|
for (const action of actions) {
|
|
635
639
|
switch (action.action) {
|
|
636
640
|
case "submit": {
|
|
637
|
-
const query = this.editor.
|
|
641
|
+
const query = this.editor.text.trim();
|
|
638
642
|
if (!query) {
|
|
639
643
|
this.hide();
|
|
640
644
|
return;
|
|
@@ -688,8 +692,8 @@ export class FloatingPanel {
|
|
|
688
692
|
width: geo.contentW,
|
|
689
693
|
height: geo.contentH,
|
|
690
694
|
phase: this.phase,
|
|
691
|
-
inputBuffer: this.editor.
|
|
692
|
-
inputCursor: this.editor.
|
|
695
|
+
inputBuffer: this.editor.displayText,
|
|
696
|
+
inputCursor: this.editor.displayCursor,
|
|
693
697
|
scrollOffset: this.scrollOffset,
|
|
694
698
|
contentLines: this.contentLines,
|
|
695
699
|
partialLine: this.currentPartialLine,
|
|
@@ -752,23 +756,35 @@ export class FloatingPanel {
|
|
|
752
756
|
this.resizeHandler = null;
|
|
753
757
|
}
|
|
754
758
|
this.suppressNextRedraw = true;
|
|
759
|
+
// Re-check alt screen state: the program we overlaid may have exited
|
|
760
|
+
// (e.g. agent quit vim via terminal_keys) while the panel was active.
|
|
761
|
+
const stillInAltScreen = !this.usedAltScreen && !!this.buffer?.altScreen;
|
|
762
|
+
const programExited = !this.usedAltScreen && !stillInAltScreen;
|
|
755
763
|
if (this.usedAltScreen) {
|
|
756
764
|
process.stdout.write("\x1b[?1049l");
|
|
757
765
|
}
|
|
758
|
-
//
|
|
759
|
-
//
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
this.bus.emit("shell:pty-resize", { cols, rows });
|
|
765
|
-
}, 50);
|
|
766
|
-
if (!this.buffer && this.ptyBuffer) {
|
|
766
|
+
// Replay PTY output that arrived while the overlay was active.
|
|
767
|
+
// Without this, commands run by the agent (e.g. user_shell ls)
|
|
768
|
+
// would vanish — the alt screen exit restores the saved screen
|
|
769
|
+
// from before the overlay opened, losing any shell output produced
|
|
770
|
+
// during the session.
|
|
771
|
+
if (this.ptyBuffer) {
|
|
767
772
|
process.stdout.write(this.ptyBuffer);
|
|
768
773
|
}
|
|
769
774
|
this.ptyBuffer = "";
|
|
770
|
-
this.bus.emit("shell:stdout-hide", {});
|
|
771
775
|
this.bus.emit("shell:stdout-release", {});
|
|
776
|
+
if (stillInAltScreen || programExited) {
|
|
777
|
+
// Either a TUI app is still running and needs SIGWINCH to repaint,
|
|
778
|
+
// or the overlaid program exited (e.g. agent quit vim) and we
|
|
779
|
+
// discarded its stale buffer — SIGWINCH makes the shell redraw
|
|
780
|
+
// its prompt cleanly.
|
|
781
|
+
const cols = process.stdout.columns || 80;
|
|
782
|
+
const rows = process.stdout.rows || 24;
|
|
783
|
+
this.bus.emit("shell:pty-resize", { cols, rows: rows - 1 });
|
|
784
|
+
setTimeout(() => {
|
|
785
|
+
this.bus.emit("shell:pty-resize", { cols, rows });
|
|
786
|
+
}, 50);
|
|
787
|
+
}
|
|
772
788
|
}
|
|
773
789
|
// ── Passthrough rendering ─────────────────────────────────
|
|
774
790
|
/** Start rendering TerminalBuffer directly (no overlay box). */
|
|
@@ -11,31 +11,52 @@
|
|
|
11
11
|
* if (lang === "latex") return renderLatex(code);
|
|
12
12
|
* return next(lang, code); // call original
|
|
13
13
|
* });
|
|
14
|
+
*
|
|
15
|
+
* Internally, each handler is stored as a base function plus an ordered
|
|
16
|
+
* list of advisors. `call` builds the chain on invocation, so advisors
|
|
17
|
+
* can be added or removed at any time without closure entanglement.
|
|
14
18
|
*/
|
|
19
|
+
type HandlerFn = (...args: any[]) => any;
|
|
20
|
+
type Advisor = (next: HandlerFn, ...args: any[]) => any;
|
|
21
|
+
/** The subset of HandlerRegistry methods available to extensions. */
|
|
22
|
+
export interface HandlerFunctions {
|
|
23
|
+
define(name: string, fn: (...args: any[]) => any): void;
|
|
24
|
+
advise(name: string, advisor: (next: (...args: any[]) => any, ...args: any[]) => any): () => void;
|
|
25
|
+
call(name: string, ...args: any[]): any;
|
|
26
|
+
list(): string[];
|
|
27
|
+
}
|
|
15
28
|
export declare class HandlerRegistry {
|
|
16
|
-
private
|
|
29
|
+
private entries;
|
|
17
30
|
/**
|
|
18
|
-
* Register a named handler. If one already exists,
|
|
31
|
+
* Register a named handler. If one already exists, its base is replaced
|
|
32
|
+
* but existing advisors are preserved.
|
|
19
33
|
*/
|
|
20
|
-
define(name: string, fn:
|
|
34
|
+
define(name: string, fn: HandlerFn): void;
|
|
21
35
|
/**
|
|
22
|
-
*
|
|
23
|
-
*
|
|
36
|
+
* Add an advisor to a named handler. The advisor receives `next`
|
|
37
|
+
* (the rest of the chain) and all original arguments.
|
|
24
38
|
*
|
|
25
|
-
* - Call `next(...args)` to invoke the
|
|
39
|
+
* - Call `next(...args)` to invoke the rest of the chain
|
|
26
40
|
* - Don't call `next` to replace entirely (override)
|
|
27
41
|
* - Call `next` conditionally to wrap (around)
|
|
28
42
|
*
|
|
29
|
-
*
|
|
30
|
-
*
|
|
43
|
+
* Advisors run outermost-first (last added = outermost).
|
|
44
|
+
* Returns an unadvise function that cleanly removes this advisor.
|
|
31
45
|
*/
|
|
32
|
-
advise(name: string,
|
|
46
|
+
advise(name: string, advisor: Advisor): () => void;
|
|
33
47
|
/**
|
|
34
|
-
* Call a named handler.
|
|
48
|
+
* Call a named handler. Builds the advisor chain on each call:
|
|
49
|
+
* outermost advisor wraps the next, down to the base handler.
|
|
50
|
+
* Returns undefined if no handler is registered.
|
|
35
51
|
*/
|
|
36
52
|
call(name: string, ...args: any[]): any;
|
|
37
53
|
/**
|
|
38
54
|
* Check if a named handler exists.
|
|
39
55
|
*/
|
|
40
56
|
has(name: string): boolean;
|
|
57
|
+
/**
|
|
58
|
+
* Names of all registered handlers. For diagnostic/introspection use.
|
|
59
|
+
*/
|
|
60
|
+
list(): string[];
|
|
41
61
|
}
|
|
62
|
+
export {};
|
|
@@ -11,42 +11,84 @@
|
|
|
11
11
|
* if (lang === "latex") return renderLatex(code);
|
|
12
12
|
* return next(lang, code); // call original
|
|
13
13
|
* });
|
|
14
|
+
*
|
|
15
|
+
* Internally, each handler is stored as a base function plus an ordered
|
|
16
|
+
* list of advisors. `call` builds the chain on invocation, so advisors
|
|
17
|
+
* can be added or removed at any time without closure entanglement.
|
|
14
18
|
*/
|
|
15
|
-
/* eslint-disable @typescript-eslint/no-explicit-any */
|
|
16
19
|
export class HandlerRegistry {
|
|
17
|
-
|
|
20
|
+
entries = new Map();
|
|
18
21
|
/**
|
|
19
|
-
* Register a named handler. If one already exists,
|
|
22
|
+
* Register a named handler. If one already exists, its base is replaced
|
|
23
|
+
* but existing advisors are preserved.
|
|
20
24
|
*/
|
|
21
25
|
define(name, fn) {
|
|
22
|
-
this.
|
|
26
|
+
const existing = this.entries.get(name);
|
|
27
|
+
if (existing) {
|
|
28
|
+
existing.base = fn;
|
|
29
|
+
}
|
|
30
|
+
else {
|
|
31
|
+
this.entries.set(name, { base: fn, advisors: [] });
|
|
32
|
+
}
|
|
23
33
|
}
|
|
24
34
|
/**
|
|
25
|
-
*
|
|
26
|
-
*
|
|
35
|
+
* Add an advisor to a named handler. The advisor receives `next`
|
|
36
|
+
* (the rest of the chain) and all original arguments.
|
|
27
37
|
*
|
|
28
|
-
* - Call `next(...args)` to invoke the
|
|
38
|
+
* - Call `next(...args)` to invoke the rest of the chain
|
|
29
39
|
* - Don't call `next` to replace entirely (override)
|
|
30
40
|
* - Call `next` conditionally to wrap (around)
|
|
31
41
|
*
|
|
32
|
-
*
|
|
33
|
-
*
|
|
42
|
+
* Advisors run outermost-first (last added = outermost).
|
|
43
|
+
* Returns an unadvise function that cleanly removes this advisor.
|
|
34
44
|
*/
|
|
35
|
-
advise(name,
|
|
36
|
-
|
|
37
|
-
|
|
45
|
+
advise(name, advisor) {
|
|
46
|
+
let entry = this.entries.get(name);
|
|
47
|
+
if (!entry) {
|
|
48
|
+
entry = { base: (() => undefined), advisors: [] };
|
|
49
|
+
this.entries.set(name, entry);
|
|
50
|
+
}
|
|
51
|
+
entry.advisors.push(advisor);
|
|
52
|
+
let removed = false;
|
|
53
|
+
return () => {
|
|
54
|
+
if (removed)
|
|
55
|
+
return;
|
|
56
|
+
removed = true;
|
|
57
|
+
const e = this.entries.get(name);
|
|
58
|
+
if (!e)
|
|
59
|
+
return;
|
|
60
|
+
const idx = e.advisors.indexOf(advisor);
|
|
61
|
+
if (idx !== -1)
|
|
62
|
+
e.advisors.splice(idx, 1);
|
|
63
|
+
};
|
|
38
64
|
}
|
|
39
65
|
/**
|
|
40
|
-
* Call a named handler.
|
|
66
|
+
* Call a named handler. Builds the advisor chain on each call:
|
|
67
|
+
* outermost advisor wraps the next, down to the base handler.
|
|
68
|
+
* Returns undefined if no handler is registered.
|
|
41
69
|
*/
|
|
42
70
|
call(name, ...args) {
|
|
43
|
-
const
|
|
44
|
-
|
|
71
|
+
const entry = this.entries.get(name);
|
|
72
|
+
if (!entry)
|
|
73
|
+
return undefined;
|
|
74
|
+
// Build chain: base ← advisor[0] ← advisor[1] ← ... ← advisor[n-1]
|
|
75
|
+
let fn = entry.base;
|
|
76
|
+
for (const advisor of entry.advisors) {
|
|
77
|
+
const next = fn;
|
|
78
|
+
fn = (...a) => advisor(next, ...a);
|
|
79
|
+
}
|
|
80
|
+
return fn(...args);
|
|
45
81
|
}
|
|
46
82
|
/**
|
|
47
83
|
* Check if a named handler exists.
|
|
48
84
|
*/
|
|
49
85
|
has(name) {
|
|
50
|
-
return this.
|
|
86
|
+
return this.entries.has(name);
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* Names of all registered handlers. For diagnostic/introspection use.
|
|
90
|
+
*/
|
|
91
|
+
list() {
|
|
92
|
+
return [...this.entries.keys()];
|
|
51
93
|
}
|
|
52
94
|
}
|
|
@@ -2,8 +2,14 @@
|
|
|
2
2
|
* Minimal line editor with readline-style keybindings.
|
|
3
3
|
*
|
|
4
4
|
* Pure logic — no I/O, no rendering, no event bus. Consumers feed raw
|
|
5
|
-
* terminal input bytes and receive high-level actions back.
|
|
6
|
-
*
|
|
5
|
+
* terminal input bytes and receive high-level actions back.
|
|
6
|
+
*
|
|
7
|
+
* The internal buffer may contain PUA placeholder characters for pasted
|
|
8
|
+
* multi-line content. Consumers should use the typed accessors:
|
|
9
|
+
* - `text` — resolved content (pastes expanded), for submit/history/logic
|
|
10
|
+
* - `displayText` — display content (pastes collapsed to labels), for rendering
|
|
11
|
+
* - `displayCursor` — cursor column in display coordinates
|
|
12
|
+
* - `setText()` — replace buffer content (clears paste attachments)
|
|
7
13
|
*/
|
|
8
14
|
export type LineEditAction = {
|
|
9
15
|
action: "changed";
|
|
@@ -24,12 +30,28 @@ export type LineEditAction = {
|
|
|
24
30
|
action: "arrow-down";
|
|
25
31
|
};
|
|
26
32
|
export declare class LineEditor {
|
|
27
|
-
|
|
33
|
+
private _buf;
|
|
28
34
|
cursor: number;
|
|
29
35
|
private pendingSeq;
|
|
36
|
+
private inPaste;
|
|
37
|
+
private pasteAccum;
|
|
38
|
+
private pastes;
|
|
39
|
+
private pasteCounter;
|
|
30
40
|
private history;
|
|
31
41
|
private historyIndex;
|
|
32
42
|
private savedBuffer;
|
|
43
|
+
/** Resolved text — paste placeholders expanded. For submit, history, logic. */
|
|
44
|
+
get text(): string;
|
|
45
|
+
/** Display text — paste placeholders replaced with labels. For rendering. */
|
|
46
|
+
get displayText(): string;
|
|
47
|
+
/** Cursor position mapped to display-text character offset. */
|
|
48
|
+
get displayCursor(): number;
|
|
49
|
+
/** Cursor position as visible terminal-column width (accounts for CJK etc.). */
|
|
50
|
+
get displayCursorWidth(): number;
|
|
51
|
+
/** Number of logical positions in the buffer. */
|
|
52
|
+
get length(): number;
|
|
53
|
+
/** Replace buffer content. Clears paste attachments. */
|
|
54
|
+
setText(value: string): void;
|
|
33
55
|
/** Process raw terminal input, return actions for the consumer. */
|
|
34
56
|
feed(data: string): LineEditAction[];
|
|
35
57
|
/** Check if there's a pending incomplete escape sequence. */
|
|
@@ -53,6 +75,14 @@ export declare class LineEditor {
|
|
|
53
75
|
private handleKittyKey;
|
|
54
76
|
private insertAt;
|
|
55
77
|
private moveTo;
|
|
78
|
+
/** Move cursor to start of the current logical line. */
|
|
79
|
+
private moveToLineStart;
|
|
80
|
+
/** Move cursor to end of the current logical line. */
|
|
81
|
+
private moveToLineEnd;
|
|
82
|
+
/** Delete from start of current logical line to cursor (Ctrl+U). */
|
|
83
|
+
private deleteLineStart;
|
|
84
|
+
/** Delete from cursor to end of current logical line (Ctrl+K). */
|
|
85
|
+
private deleteLineEnd;
|
|
56
86
|
private deleteBackward;
|
|
57
87
|
private deleteForward;
|
|
58
88
|
private deleteRange;
|