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/settings.d.ts
CHANGED
|
@@ -50,20 +50,46 @@ export interface Settings {
|
|
|
50
50
|
historyMaxBytes?: number;
|
|
51
51
|
/** Number of prior history entries to load on startup (default: 50). */
|
|
52
52
|
historyStartupEntries?: number;
|
|
53
|
-
/**
|
|
54
|
-
|
|
53
|
+
/** Auto-compact threshold as fraction of conversation budget (0-1, default 0.5). */
|
|
54
|
+
autoCompactThreshold?: number;
|
|
55
55
|
/** Max command output lines shown inline in TUI. */
|
|
56
56
|
maxCommandOutputLines?: number;
|
|
57
57
|
/** Max read tool output lines shown inline in TUI (0 = hide). */
|
|
58
58
|
readOutputMaxLines?: number;
|
|
59
|
-
/** Max diff lines
|
|
59
|
+
/** Max diff lines rendered in the TUI (Infinity = no limit). */
|
|
60
60
|
diffMaxLines?: number;
|
|
61
|
+
/** Tool protocol:
|
|
62
|
+
* "api" — all tools sent with full schema.
|
|
63
|
+
* "deferred" — extensions dispatched through `use_extension(name, args)` meta-tool.
|
|
64
|
+
* "deferred-lookup" — extensions loaded on demand via `load_tool(names[])`; once loaded, callable as first-class tools.
|
|
65
|
+
* "inline" — tools described as text.
|
|
66
|
+
*/
|
|
67
|
+
toolMode?: "api" | "deferred" | "deferred-lookup" | "inline";
|
|
61
68
|
/** Additional directories to scan for skills (supports ~ expansion). */
|
|
62
69
|
skillPaths?: string[];
|
|
70
|
+
/**
|
|
71
|
+
* Enable the "diagnose" tool — lets the agent evaluate JavaScript
|
|
72
|
+
* expressions against its own runtime state. Powerful for introspection
|
|
73
|
+
* (e.g. this.conversation.turns.length) but grants arbitrary code
|
|
74
|
+
* execution within the agent process. Off by default because the
|
|
75
|
+
* agent already has unrestricted bash access — this is a convenience,
|
|
76
|
+
* not a new capability.
|
|
77
|
+
*/
|
|
78
|
+
diagnose?: boolean;
|
|
63
79
|
/** Show a startup banner when agent-sh launches. */
|
|
64
80
|
startupBanner?: boolean;
|
|
65
81
|
/** Show a subtle agent-sh indicator in the shell prompt. */
|
|
66
82
|
promptIndicator?: boolean;
|
|
83
|
+
/** Names of built-in extensions to disable (e.g. ["command-suggest"]). */
|
|
84
|
+
disabledBuiltins?: string[];
|
|
85
|
+
/**
|
|
86
|
+
* Names of user extensions in ~/.agent-sh/extensions/ to skip when
|
|
87
|
+
* auto-discovering. Match by basename without extension for files
|
|
88
|
+
* (e.g. "peer-mesh" matches peer-mesh.ts), or by directory name for
|
|
89
|
+
* directory-style extensions (e.g. "superash" matches superash/index.ts).
|
|
90
|
+
* Beats having to rename files to .disabled every time.
|
|
91
|
+
*/
|
|
92
|
+
disabledExtensions?: string[];
|
|
67
93
|
}
|
|
68
94
|
declare const DEFAULTS: Required<Settings>;
|
|
69
95
|
/** Load settings from disk (cached after first call). */
|
|
@@ -81,6 +107,17 @@ export declare function getSettings(): Settings & typeof DEFAULTS;
|
|
|
81
107
|
export declare function getExtensionSettings<T extends Record<string, unknown>>(namespace: string, defaults: T): T;
|
|
82
108
|
/** Reset cached settings (for testing or after external edit). */
|
|
83
109
|
export declare function reloadSettings(): void;
|
|
110
|
+
/**
|
|
111
|
+
* Deep-merge a patch into ~/.agent-sh/settings.json on disk.
|
|
112
|
+
*
|
|
113
|
+
* Reads the raw file (preserving unknown keys), merges the patch, writes back
|
|
114
|
+
* with 2-space indentation, and clears the cache so subsequent getSettings()
|
|
115
|
+
* calls see the new values.
|
|
116
|
+
*
|
|
117
|
+
* Used by runtime controls (`/model`, `/backend`) that want their selection
|
|
118
|
+
* to persist as the default across restarts.
|
|
119
|
+
*/
|
|
120
|
+
export declare function updateSettings(patch: Record<string, unknown>): void;
|
|
84
121
|
/**
|
|
85
122
|
* Expand $ENV_VAR references in a string.
|
|
86
123
|
* Supports $VAR and ${VAR} syntax.
|
package/dist/settings.js
CHANGED
|
@@ -14,23 +14,27 @@ const DEFAULTS = {
|
|
|
14
14
|
historySize: 500,
|
|
15
15
|
providers: {},
|
|
16
16
|
defaultProvider: undefined,
|
|
17
|
-
defaultBackend: "
|
|
17
|
+
defaultBackend: "ash",
|
|
18
|
+
toolMode: "api",
|
|
18
19
|
contextWindowSize: 20,
|
|
19
|
-
contextBudget:
|
|
20
|
-
shellTruncateThreshold:
|
|
21
|
-
shellHeadLines:
|
|
22
|
-
shellTailLines:
|
|
23
|
-
recallExpandMaxLines:
|
|
20
|
+
contextBudget: 32768,
|
|
21
|
+
shellTruncateThreshold: 20,
|
|
22
|
+
shellHeadLines: 10,
|
|
23
|
+
shellTailLines: 10,
|
|
24
|
+
recallExpandMaxLines: 500,
|
|
24
25
|
shellContextRatio: 0.35,
|
|
25
|
-
historyMaxBytes:
|
|
26
|
-
historyStartupEntries:
|
|
27
|
-
|
|
26
|
+
historyMaxBytes: 104857600, // 100MB — history is only accessed via search/expand, never loaded wholesale
|
|
27
|
+
historyStartupEntries: 100,
|
|
28
|
+
autoCompactThreshold: 0.5,
|
|
28
29
|
maxCommandOutputLines: 3,
|
|
29
30
|
readOutputMaxLines: 10,
|
|
30
|
-
diffMaxLines:
|
|
31
|
+
diffMaxLines: Infinity,
|
|
31
32
|
skillPaths: [],
|
|
33
|
+
diagnose: false,
|
|
32
34
|
startupBanner: true,
|
|
33
35
|
promptIndicator: true,
|
|
36
|
+
disabledBuiltins: [],
|
|
37
|
+
disabledExtensions: [],
|
|
34
38
|
};
|
|
35
39
|
let cached = null;
|
|
36
40
|
/** Load settings from disk (cached after first call). */
|
|
@@ -71,6 +75,49 @@ export function getExtensionSettings(namespace, defaults) {
|
|
|
71
75
|
export function reloadSettings() {
|
|
72
76
|
cached = null;
|
|
73
77
|
}
|
|
78
|
+
/**
|
|
79
|
+
* Deep-merge a patch into ~/.agent-sh/settings.json on disk.
|
|
80
|
+
*
|
|
81
|
+
* Reads the raw file (preserving unknown keys), merges the patch, writes back
|
|
82
|
+
* with 2-space indentation, and clears the cache so subsequent getSettings()
|
|
83
|
+
* calls see the new values.
|
|
84
|
+
*
|
|
85
|
+
* Used by runtime controls (`/model`, `/backend`) that want their selection
|
|
86
|
+
* to persist as the default across restarts.
|
|
87
|
+
*/
|
|
88
|
+
export function updateSettings(patch) {
|
|
89
|
+
let existing = {};
|
|
90
|
+
try {
|
|
91
|
+
const raw = fs.readFileSync(SETTINGS_PATH, "utf-8");
|
|
92
|
+
existing = JSON.parse(raw);
|
|
93
|
+
}
|
|
94
|
+
catch {
|
|
95
|
+
// file missing or unreadable — start fresh
|
|
96
|
+
}
|
|
97
|
+
const merged = deepMerge(existing, patch);
|
|
98
|
+
try {
|
|
99
|
+
fs.mkdirSync(CONFIG_DIR, { recursive: true });
|
|
100
|
+
fs.writeFileSync(SETTINGS_PATH, JSON.stringify(merged, null, 2) + "\n", "utf-8");
|
|
101
|
+
cached = null;
|
|
102
|
+
}
|
|
103
|
+
catch (err) {
|
|
104
|
+
console.error(`[agent-sh] Warning: failed to update ${SETTINGS_PATH}: ${err.message}`);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
function deepMerge(target, source) {
|
|
108
|
+
const out = { ...target };
|
|
109
|
+
for (const [key, val] of Object.entries(source)) {
|
|
110
|
+
const existing = out[key];
|
|
111
|
+
if (val !== null && typeof val === "object" && !Array.isArray(val) &&
|
|
112
|
+
existing !== null && typeof existing === "object" && !Array.isArray(existing)) {
|
|
113
|
+
out[key] = deepMerge(existing, val);
|
|
114
|
+
}
|
|
115
|
+
else {
|
|
116
|
+
out[key] = val;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
return out;
|
|
120
|
+
}
|
|
74
121
|
/**
|
|
75
122
|
* Expand $ENV_VAR references in a string.
|
|
76
123
|
* Supports $VAR and ${VAR} syntax.
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { EventBus } from "
|
|
1
|
+
import type { EventBus } from "../event-bus.js";
|
|
2
2
|
/**
|
|
3
3
|
* Narrow contract between InputHandler and its host (Shell).
|
|
4
4
|
* InputHandler never touches the PTY or EventBus directly —
|
|
@@ -28,7 +28,8 @@ export declare class InputHandler {
|
|
|
28
28
|
private history;
|
|
29
29
|
private historyIndex;
|
|
30
30
|
private savedBuffer;
|
|
31
|
-
private
|
|
31
|
+
private cursorRowsBelow;
|
|
32
|
+
private cursorTermCol;
|
|
32
33
|
private escapeTimer;
|
|
33
34
|
private bus;
|
|
34
35
|
private onShowAgentInfo;
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import * as fs from "node:fs";
|
|
2
2
|
import * as path from "node:path";
|
|
3
|
-
import { visibleLen } from "
|
|
4
|
-
import { palette as p } from "
|
|
5
|
-
import { LineEditor } from "
|
|
6
|
-
import { CONFIG_DIR, getSettings } from "
|
|
7
|
-
const HISTORY_FILE = path.join(CONFIG_DIR, "history");
|
|
3
|
+
import { visibleLen } from "../utils/ansi.js";
|
|
4
|
+
import { palette as p } from "../utils/palette.js";
|
|
5
|
+
import { LineEditor } from "../utils/line-editor.js";
|
|
6
|
+
import { CONFIG_DIR, getSettings } from "../settings.js";
|
|
7
|
+
const HISTORY_FILE = path.join(CONFIG_DIR, "input-history");
|
|
8
8
|
export class InputHandler {
|
|
9
9
|
ctx;
|
|
10
10
|
lineBuffer = "";
|
|
@@ -20,7 +20,8 @@ export class InputHandler {
|
|
|
20
20
|
history = [];
|
|
21
21
|
historyIndex = -1; // -1 = not browsing history
|
|
22
22
|
savedBuffer = ""; // buffer saved when entering history
|
|
23
|
-
|
|
23
|
+
cursorRowsBelow = 0; // rows from prompt top to cursor row
|
|
24
|
+
cursorTermCol = 1; // 1-indexed terminal column of cursor
|
|
24
25
|
escapeTimer = null;
|
|
25
26
|
bus;
|
|
26
27
|
onShowAgentInfo;
|
|
@@ -72,9 +73,10 @@ export class InputHandler {
|
|
|
72
73
|
/** Write the mode prompt line with cursor at the correct position. */
|
|
73
74
|
writeModePromptLine(showBuffer = true) {
|
|
74
75
|
const termW = process.stdout.columns || 80;
|
|
75
|
-
// Move cursor to the start of the prompt area
|
|
76
|
-
|
|
77
|
-
|
|
76
|
+
// Move cursor to the start of the prompt area.
|
|
77
|
+
// We know exactly how many rows below the top the cursor currently sits.
|
|
78
|
+
if (this.cursorRowsBelow > 0) {
|
|
79
|
+
process.stdout.write(`\x1b[${this.cursorRowsBelow}A`);
|
|
78
80
|
}
|
|
79
81
|
// Clear from here to end of screen — removes current + all wrapped lines below
|
|
80
82
|
process.stdout.write("\r\x1b[J");
|
|
@@ -86,38 +88,39 @@ export class InputHandler {
|
|
|
86
88
|
const icon = this.activeMode?.promptIcon ?? "❯";
|
|
87
89
|
const promptPrefix = infoPrefix + p.warning + p.bold + icon + " " + p.reset;
|
|
88
90
|
const promptVisLen = visibleLen(infoPrefix) + visibleLen(icon) + 1; // icon + space
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
const
|
|
95
|
-
this.
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
91
|
+
const display = showBuffer ? this.editor.displayText : "";
|
|
92
|
+
const dCursor = showBuffer ? this.editor.displayCursor : 0;
|
|
93
|
+
if (!showBuffer) {
|
|
94
|
+
// No buffer — just write the prompt prefix, cursor stays at end
|
|
95
|
+
process.stdout.write(promptPrefix);
|
|
96
|
+
const N = promptVisLen;
|
|
97
|
+
this.cursorRowsBelow = N > 0 ? Math.ceil(N / termW) - 1 : 0;
|
|
98
|
+
this.cursorTermCol = N === 0 ? 1 : (N % termW === 0 ? termW : (N % termW) + 1);
|
|
99
|
+
}
|
|
100
|
+
else if (!display.includes("\n")) {
|
|
101
|
+
// Single-line: write up to cursor, save, write rest, restore.
|
|
102
|
+
// The terminal handles all wrapping — no manual row/col math needed.
|
|
103
|
+
const before = display.slice(0, dCursor);
|
|
104
|
+
const after = display.slice(dCursor);
|
|
105
|
+
process.stdout.write(promptPrefix + p.accent + before + p.reset +
|
|
106
|
+
"\x1b7" + // DECSC — save cursor position
|
|
107
|
+
p.accent + after + p.reset +
|
|
108
|
+
"\x1b8" // DECRC — restore cursor position
|
|
109
|
+
);
|
|
110
|
+
// Clearing on next redraw needs total rows, so measure the full
|
|
111
|
+
// content width — not just up to the cursor.
|
|
112
|
+
const totalVisLen = promptVisLen + visibleLen(display);
|
|
113
|
+
this.cursorRowsBelow = totalVisLen > 0 ? Math.ceil(totalVisLen / termW) - 1 : 0;
|
|
114
|
+
const cursorVisCol = promptVisLen + visibleLen(before);
|
|
115
|
+
this.cursorTermCol = cursorVisCol === 0 ? 1 : (cursorVisCol % termW === 0 ? termW : (cursorVisCol % termW) + 1);
|
|
101
116
|
}
|
|
102
117
|
else {
|
|
103
|
-
// Multi-line: render each line with continuation indent
|
|
104
|
-
|
|
118
|
+
// Multi-line: render each line with continuation indent.
|
|
119
|
+
// Same save/restore strategy — cursor position is never computed.
|
|
120
|
+
const lines = display.split("\n");
|
|
105
121
|
const indent = " ".repeat(promptVisLen);
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
const prefix = li === 0 ? promptPrefix : indent;
|
|
109
|
-
const prefixVisLen = li === 0 ? promptVisLen : promptVisLen;
|
|
110
|
-
const lineText = lines[li];
|
|
111
|
-
process.stdout.write(prefix + p.accent + lineText + p.reset);
|
|
112
|
-
if (li < lines.length - 1)
|
|
113
|
-
process.stdout.write("\n");
|
|
114
|
-
// Count terminal lines this logical line occupies
|
|
115
|
-
const lineVisLen = prefixVisLen + lineText.length;
|
|
116
|
-
totalTermLines += lineVisLen > 0 ? Math.ceil(lineVisLen / termW) : 1;
|
|
117
|
-
}
|
|
118
|
-
this.promptWrappedLines = totalTermLines - 1;
|
|
119
|
-
// Position cursor: find which line and column the cursor is on
|
|
120
|
-
let charsRemaining = this.editor.cursor;
|
|
122
|
+
// Locate cursor: which logical line and offset within it.
|
|
123
|
+
let charsRemaining = dCursor;
|
|
121
124
|
let cursorLine = 0;
|
|
122
125
|
for (let li = 0; li < lines.length; li++) {
|
|
123
126
|
if (charsRemaining <= lines[li].length) {
|
|
@@ -127,13 +130,35 @@ export class InputHandler {
|
|
|
127
130
|
charsRemaining -= lines[li].length + 1; // +1 for \n
|
|
128
131
|
cursorLine = li + 1;
|
|
129
132
|
}
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
133
|
+
let output = "";
|
|
134
|
+
let cursorRowFromTop = 0;
|
|
135
|
+
let rowsSoFar = 0;
|
|
136
|
+
for (let li = 0; li < lines.length; li++) {
|
|
137
|
+
const prefix = li === 0 ? promptPrefix : indent;
|
|
138
|
+
const lineText = lines[li];
|
|
139
|
+
const lineVisLen = promptVisLen + visibleLen(lineText);
|
|
140
|
+
const lineTermRows = lineVisLen > 0 ? Math.ceil(lineVisLen / termW) : 1;
|
|
141
|
+
if (li === cursorLine) {
|
|
142
|
+
// Split this line at the cursor.
|
|
143
|
+
const before = lineText.slice(0, charsRemaining);
|
|
144
|
+
const after = lineText.slice(charsRemaining);
|
|
145
|
+
output += prefix + p.accent + before + p.reset;
|
|
146
|
+
output += "\x1b7"; // DECSC — save cursor position
|
|
147
|
+
output += p.accent + after + p.reset;
|
|
148
|
+
const beforeVisCol = promptVisLen + visibleLen(before);
|
|
149
|
+
cursorRowFromTop = rowsSoFar + (beforeVisCol > 0 ? Math.ceil(beforeVisCol / termW) - 1 : 0);
|
|
150
|
+
this.cursorTermCol = beforeVisCol === 0 ? 1 : (beforeVisCol % termW === 0 ? termW : (beforeVisCol % termW) + 1);
|
|
151
|
+
}
|
|
152
|
+
else {
|
|
153
|
+
output += prefix + p.accent + lineText + p.reset;
|
|
154
|
+
}
|
|
155
|
+
if (li < lines.length - 1)
|
|
156
|
+
output += "\n";
|
|
157
|
+
rowsSoFar += lineTermRows;
|
|
134
158
|
}
|
|
135
|
-
|
|
136
|
-
|
|
159
|
+
process.stdout.write(output + "\x1b8"); // DECRC — restore cursor position
|
|
160
|
+
// Total rows (not cursor row) so next redraw clears the whole area.
|
|
161
|
+
this.cursorRowsBelow = rowsSoFar - 1 > 0 ? rowsSoFar - 1 : 0;
|
|
137
162
|
}
|
|
138
163
|
}
|
|
139
164
|
handleInput(data) {
|
|
@@ -249,26 +274,29 @@ export class InputHandler {
|
|
|
249
274
|
this.activeMode = mode;
|
|
250
275
|
this.editor.clear();
|
|
251
276
|
// Enable kitty keyboard protocol (progressive enhancement flag 1)
|
|
252
|
-
// so Shift+Enter sends \x1b[13;2u instead of plain \r
|
|
253
|
-
|
|
277
|
+
// so Shift+Enter sends \x1b[13;2u instead of plain \r.
|
|
278
|
+
// Enable bracket paste mode so pasted text doesn't trigger submit.
|
|
279
|
+
process.stdout.write("\x1b[>1u\x1b[?2004h");
|
|
254
280
|
this.writeModePromptLine(false);
|
|
255
281
|
}
|
|
256
282
|
exitMode() {
|
|
257
283
|
this.dismissAutocomplete();
|
|
258
284
|
this.activeMode = null;
|
|
259
285
|
this.editor.clear();
|
|
260
|
-
// Disable kitty keyboard protocol
|
|
261
|
-
process.stdout.write("\x1b[<u");
|
|
286
|
+
// Disable kitty keyboard protocol and bracket paste mode
|
|
287
|
+
process.stdout.write("\x1b[<u\x1b[?2004l");
|
|
262
288
|
this.clearPromptArea();
|
|
289
|
+
this.cursorRowsBelow = 0;
|
|
290
|
+
this.cursorTermCol = 1;
|
|
263
291
|
this.printPrompt();
|
|
264
292
|
}
|
|
265
293
|
/** Move to the start of the prompt area and clear everything below. */
|
|
266
294
|
clearPromptArea() {
|
|
267
|
-
if (this.
|
|
268
|
-
process.stdout.write(`\x1b[${this.
|
|
295
|
+
if (this.cursorRowsBelow > 0) {
|
|
296
|
+
process.stdout.write(`\x1b[${this.cursorRowsBelow}A`);
|
|
269
297
|
}
|
|
270
298
|
process.stdout.write("\r\x1b[J");
|
|
271
|
-
this.
|
|
299
|
+
this.cursorRowsBelow = 0;
|
|
272
300
|
}
|
|
273
301
|
printPrompt() {
|
|
274
302
|
this.ctx.redrawPrompt();
|
|
@@ -294,7 +322,7 @@ export class InputHandler {
|
|
|
294
322
|
this.updateAutocomplete();
|
|
295
323
|
}
|
|
296
324
|
updateAutocomplete() {
|
|
297
|
-
const buf = this.editor.
|
|
325
|
+
const buf = this.editor.text;
|
|
298
326
|
let command = null;
|
|
299
327
|
let commandArgs = null;
|
|
300
328
|
if (buf.startsWith("/")) {
|
|
@@ -342,16 +370,10 @@ export class InputHandler {
|
|
|
342
370
|
if (this.autocompleteLines > 0) {
|
|
343
371
|
process.stdout.write(`\x1b[${this.autocompleteLines}A`);
|
|
344
372
|
}
|
|
345
|
-
//
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
? `${agentInfo.info} ${indicator} `
|
|
350
|
-
: `${indicator} `;
|
|
351
|
-
const icon = this.activeMode?.promptIcon ?? "❯";
|
|
352
|
-
const promptVisLen = visibleLen(infoPrefix) + visibleLen(icon) + 1;
|
|
353
|
-
const col = promptVisLen + this.editor.cursor;
|
|
354
|
-
process.stdout.write(`\r\x1b[${col}C`);
|
|
373
|
+
// Restore cursor column — use explicit column set instead of DECRC
|
|
374
|
+
// because writing \n above may have scrolled the terminal, which
|
|
375
|
+
// invalidates the absolute position saved by DECSC.
|
|
376
|
+
process.stdout.write(`\x1b[${this.cursorTermCol}G`);
|
|
355
377
|
}
|
|
356
378
|
applyAutocomplete() {
|
|
357
379
|
if (!this.autocompleteActive || this.autocompleteItems.length === 0)
|
|
@@ -359,18 +381,16 @@ export class InputHandler {
|
|
|
359
381
|
const selected = this.autocompleteItems[this.autocompleteIndex];
|
|
360
382
|
if (!selected)
|
|
361
383
|
return;
|
|
362
|
-
const atPos = this.editor.
|
|
384
|
+
const atPos = this.editor.text.lastIndexOf("@");
|
|
363
385
|
const isFileAc = atPos >= 0 &&
|
|
364
|
-
(atPos === 0 || this.editor.
|
|
365
|
-
!this.editor.
|
|
386
|
+
(atPos === 0 || this.editor.text[atPos - 1] === " ") &&
|
|
387
|
+
!this.editor.text.slice(atPos + 1).includes(" ");
|
|
366
388
|
if (isFileAc) {
|
|
367
|
-
this.editor.
|
|
368
|
-
this.editor.buffer.slice(0, atPos) + "@" + selected.name;
|
|
389
|
+
this.editor.setText(this.editor.text.slice(0, atPos) + "@" + selected.name);
|
|
369
390
|
}
|
|
370
391
|
else {
|
|
371
|
-
this.editor.
|
|
392
|
+
this.editor.setText(selected.name);
|
|
372
393
|
}
|
|
373
|
-
this.editor.cursor = this.editor.buffer.length;
|
|
374
394
|
this.clearAutocompleteLines();
|
|
375
395
|
this.autocompleteActive = false;
|
|
376
396
|
this.autocompleteItems = [];
|
|
@@ -388,11 +408,12 @@ export class InputHandler {
|
|
|
388
408
|
clearAutocompleteLines() {
|
|
389
409
|
if (this.autocompleteLines <= 0)
|
|
390
410
|
return;
|
|
391
|
-
|
|
411
|
+
// Use CSI B (cursor down, bounded) instead of \n to avoid scroll
|
|
392
412
|
for (let i = 0; i < this.autocompleteLines; i++) {
|
|
393
|
-
process.stdout.write("\
|
|
413
|
+
process.stdout.write("\x1b[B\x1b[2K"); // move down, clear line
|
|
394
414
|
}
|
|
395
|
-
|
|
415
|
+
// Move back up and restore column with relative movement (scroll-safe)
|
|
416
|
+
process.stdout.write(`\x1b[${this.autocompleteLines}A\x1b[${this.cursorTermCol}G`);
|
|
396
417
|
this.autocompleteLines = 0;
|
|
397
418
|
}
|
|
398
419
|
handleModeInput(data) {
|
|
@@ -419,8 +440,8 @@ export class InputHandler {
|
|
|
419
440
|
switch (act.action) {
|
|
420
441
|
case "changed": {
|
|
421
442
|
// If the buffer is exactly a trigger char for a different mode, switch to it
|
|
422
|
-
const switchMode = this.modes.get(this.editor.
|
|
423
|
-
if (this.editor.
|
|
443
|
+
const switchMode = this.modes.get(this.editor.text);
|
|
444
|
+
if (this.editor.text.length === 1 && switchMode && switchMode !== this.activeMode) {
|
|
424
445
|
this.dismissAutocomplete();
|
|
425
446
|
this.clearPromptArea();
|
|
426
447
|
this.activeMode = switchMode;
|
|
@@ -437,10 +458,10 @@ export class InputHandler {
|
|
|
437
458
|
if (this.autocompleteActive) {
|
|
438
459
|
this.applyAutocomplete();
|
|
439
460
|
}
|
|
440
|
-
// Use editor.
|
|
461
|
+
// Use editor.text (not act.buffer) so autocomplete selections
|
|
441
462
|
// take effect — act.buffer is a stale snapshot from before
|
|
442
|
-
// applyAutocomplete() updated the
|
|
443
|
-
const query = this.editor.
|
|
463
|
+
// applyAutocomplete() updated the editor.
|
|
464
|
+
const query = this.editor.text.trim();
|
|
444
465
|
if (query) {
|
|
445
466
|
// Add to history (avoid consecutive duplicates)
|
|
446
467
|
if (this.history.length === 0 || this.history[this.history.length - 1] !== query) {
|
|
@@ -452,17 +473,24 @@ export class InputHandler {
|
|
|
452
473
|
this.savedBuffer = "";
|
|
453
474
|
this.clearAutocompleteLines();
|
|
454
475
|
this.clearPromptArea();
|
|
455
|
-
process.stdout.write("\x1b[<u"); // disable kitty
|
|
476
|
+
process.stdout.write("\x1b[<u\x1b[?2004l"); // disable kitty + bracket paste
|
|
456
477
|
const currentMode = this.activeMode;
|
|
457
478
|
this.activeMode = null;
|
|
458
479
|
this.editor.clear();
|
|
480
|
+
this.cursorRowsBelow = 0;
|
|
481
|
+
this.cursorTermCol = 1;
|
|
459
482
|
this.dismissAutocomplete();
|
|
460
483
|
if (query && query.startsWith("/")) {
|
|
461
484
|
const spaceIdx = query.indexOf(" ");
|
|
462
485
|
const name = spaceIdx === -1 ? query : query.slice(0, spaceIdx);
|
|
463
486
|
const args = spaceIdx === -1 ? "" : query.slice(spaceIdx + 1).trim();
|
|
464
487
|
this.bus.emit("command:execute", { name, args });
|
|
465
|
-
|
|
488
|
+
if (currentMode.returnToSelf) {
|
|
489
|
+
this.enterMode(currentMode);
|
|
490
|
+
}
|
|
491
|
+
else {
|
|
492
|
+
this.ctx.freshPrompt();
|
|
493
|
+
}
|
|
466
494
|
}
|
|
467
495
|
else if (query) {
|
|
468
496
|
this.pendingReturnMode = currentMode.returnToSelf ? currentMode.id : null;
|
|
@@ -506,14 +534,13 @@ export class InputHandler {
|
|
|
506
534
|
}
|
|
507
535
|
else if (this.history.length > 0) {
|
|
508
536
|
if (this.historyIndex === -1) {
|
|
509
|
-
this.savedBuffer = this.editor.
|
|
537
|
+
this.savedBuffer = this.editor.text;
|
|
510
538
|
this.historyIndex = this.history.length - 1;
|
|
511
539
|
}
|
|
512
540
|
else if (this.historyIndex > 0) {
|
|
513
541
|
this.historyIndex--;
|
|
514
542
|
}
|
|
515
|
-
this.editor.
|
|
516
|
-
this.editor.cursor = this.editor.buffer.length;
|
|
543
|
+
this.editor.setText(this.history[this.historyIndex]);
|
|
517
544
|
this.clearAutocompleteLines();
|
|
518
545
|
this.writeModePromptLine();
|
|
519
546
|
}
|
|
@@ -531,13 +558,12 @@ export class InputHandler {
|
|
|
531
558
|
else if (this.historyIndex !== -1) {
|
|
532
559
|
if (this.historyIndex < this.history.length - 1) {
|
|
533
560
|
this.historyIndex++;
|
|
534
|
-
this.editor.
|
|
561
|
+
this.editor.setText(this.history[this.historyIndex]);
|
|
535
562
|
}
|
|
536
563
|
else {
|
|
537
564
|
this.historyIndex = -1;
|
|
538
|
-
this.editor.
|
|
565
|
+
this.editor.setText(this.savedBuffer);
|
|
539
566
|
}
|
|
540
|
-
this.editor.cursor = this.editor.buffer.length;
|
|
541
567
|
this.clearAutocompleteLines();
|
|
542
568
|
this.writeModePromptLine();
|
|
543
569
|
}
|
|
@@ -1,8 +1,13 @@
|
|
|
1
|
-
import type { EventBus } from "
|
|
1
|
+
import type { EventBus } from "../event-bus.js";
|
|
2
2
|
import { type InputContext } from "./input-handler.js";
|
|
3
|
+
export interface ShellHandlers {
|
|
4
|
+
define: (name: string, fn: (...args: any[]) => any) => void;
|
|
5
|
+
call: (name: string, ...args: any[]) => any;
|
|
6
|
+
}
|
|
3
7
|
export declare class Shell implements InputContext {
|
|
4
8
|
private ptyProcess;
|
|
5
9
|
private bus;
|
|
10
|
+
private handlers;
|
|
6
11
|
private inputHandler;
|
|
7
12
|
private outputParser;
|
|
8
13
|
private paused;
|
|
@@ -14,6 +19,7 @@ export declare class Shell implements InputContext {
|
|
|
14
19
|
private tmpDir?;
|
|
15
20
|
constructor(opts: {
|
|
16
21
|
bus: EventBus;
|
|
22
|
+
handlers: ShellHandlers;
|
|
17
23
|
onShowAgentInfo?: () => {
|
|
18
24
|
info: string;
|
|
19
25
|
model?: string;
|
|
@@ -43,7 +49,7 @@ export declare class Shell implements InputContext {
|
|
|
43
49
|
* Routed through shell:redraw-prompt pipe so extensions (e.g. overlay)
|
|
44
50
|
* can suppress it by setting `handled: true`.
|
|
45
51
|
*/
|
|
46
|
-
freshPrompt():
|
|
52
|
+
freshPrompt(): boolean;
|
|
47
53
|
onCommandEntered(command: string, cwd: string): void;
|
|
48
54
|
private setupOutput;
|
|
49
55
|
private setupInput;
|