agent-sh 0.7.0 → 0.9.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 +28 -33
- package/dist/agent/agent-loop.d.ts +31 -8
- package/dist/agent/agent-loop.js +277 -66
- package/dist/agent/conversation-state.d.ts +41 -9
- package/dist/agent/conversation-state.js +340 -17
- package/dist/agent/history-file.d.ts +36 -0
- package/dist/agent/history-file.js +167 -0
- package/dist/agent/nuclear-form.d.ts +41 -0
- package/dist/agent/nuclear-form.js +176 -0
- package/dist/agent/system-prompt.d.ts +4 -5
- package/dist/agent/system-prompt.js +16 -11
- package/dist/agent/token-budget.d.ts +13 -0
- package/dist/agent/token-budget.js +50 -0
- package/dist/agent/tool-protocol.d.ts +83 -0
- package/dist/agent/tool-protocol.js +386 -0
- package/dist/agent/tools/user-shell.js +4 -1
- package/dist/agent/types.d.ts +21 -1
- package/dist/context-manager.d.ts +0 -1
- package/dist/context-manager.js +5 -110
- package/dist/core.d.ts +7 -7
- package/dist/core.js +76 -180
- package/dist/event-bus.d.ts +40 -0
- package/dist/event-bus.js +20 -1
- package/dist/extension-loader.d.ts +5 -0
- package/dist/extension-loader.js +104 -17
- package/dist/extensions/agent-backend.d.ts +13 -0
- package/dist/extensions/agent-backend.js +167 -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 +25 -0
- package/dist/extensions/slash-commands.d.ts +1 -1
- package/dist/extensions/slash-commands.js +44 -1
- package/dist/extensions/terminal-buffer.d.ts +1 -1
- package/dist/extensions/terminal-buffer.js +22 -8
- package/dist/extensions/tui-renderer.js +177 -122
- package/dist/index.js +14 -20
- package/dist/settings.d.ts +25 -2
- package/dist/settings.js +25 -4
- package/dist/{input-handler.d.ts → shell/input-handler.d.ts} +1 -1
- package/dist/{input-handler.js → shell/input-handler.js} +60 -43
- 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} +24 -6
- package/dist/types.d.ts +49 -32
- package/dist/utils/ansi.d.ts +10 -0
- package/dist/utils/ansi.js +27 -0
- package/dist/utils/compositor.d.ts +62 -0
- package/dist/utils/compositor.js +88 -0
- package/dist/utils/diff-renderer.js +92 -4
- package/dist/utils/floating-panel.d.ts +34 -3
- package/dist/utils/floating-panel.js +315 -82
- package/dist/utils/handler-registry.d.ts +26 -10
- package/dist/utils/handler-registry.js +52 -16
- package/dist/utils/line-editor.d.ts +32 -3
- package/dist/utils/line-editor.js +218 -36
- package/dist/utils/markdown.d.ts +1 -0
- package/dist/utils/markdown.js +4 -4
- package/dist/utils/message-utils.d.ts +35 -0
- package/dist/utils/message-utils.js +75 -0
- package/dist/utils/terminal-buffer.d.ts +9 -1
- package/dist/utils/terminal-buffer.js +31 -2
- package/dist/utils/tool-display.d.ts +1 -0
- package/dist/utils/tool-display.js +1 -1
- 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 +571 -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 +154 -0
- package/examples/extensions/ash-mcp-bridge/package.json +9 -0
- package/examples/extensions/claude-code-bridge/index.ts +77 -1
- package/examples/extensions/interactive-prompts.ts +82 -110
- package/examples/extensions/overlay-agent.ts +84 -38
- package/examples/extensions/peer-mesh.ts +450 -0
- package/examples/extensions/pi-bridge/index.ts +87 -2
- package/examples/extensions/questionnaire.ts +249 -0
- package/examples/extensions/tmux-pane.ts +307 -0
- package/examples/extensions/web-access.ts +327 -0
- package/package.json +9 -1
- package/dist/extensions/overlay-agent.d.ts +0 -11
- package/dist/extensions/overlay-agent.js +0 -43
- package/examples/extensions/terminal-buffer.ts +0 -184
package/dist/types.d.ts
CHANGED
|
@@ -1,13 +1,37 @@
|
|
|
1
1
|
import type { EventBus } from "./event-bus.js";
|
|
2
2
|
import type { ContextManager } from "./context-manager.js";
|
|
3
|
-
import type { LlmClient } from "./utils/llm-client.js";
|
|
4
3
|
import type { ColorPalette } from "./utils/palette.js";
|
|
5
4
|
import type { BlockTransformOptions, FencedBlockTransformOptions } from "./utils/stream-transform.js";
|
|
6
5
|
import type { ToolDefinition } from "./agent/types.js";
|
|
7
6
|
import type { TerminalBuffer } from "./utils/terminal-buffer.js";
|
|
8
|
-
import type {
|
|
7
|
+
import type { Compositor } from "./utils/compositor.js";
|
|
9
8
|
export type { ContentBlock } from "./event-bus.js";
|
|
10
9
|
export type { BlockTransformOptions, FencedBlockTransformOptions } from "./utils/stream-transform.js";
|
|
10
|
+
export type { RenderSurface } from "./utils/compositor.js";
|
|
11
|
+
export interface RemoteSessionOptions {
|
|
12
|
+
/** The surface to render agent output to. */
|
|
13
|
+
surface: import("./utils/compositor.js").RenderSurface;
|
|
14
|
+
/** Suppress response borders (default: true). */
|
|
15
|
+
suppressBorders?: boolean;
|
|
16
|
+
/** Suppress user query box (default: false).
|
|
17
|
+
* True for sessions with their own input (rsplit, overlay).
|
|
18
|
+
* False for sessions where input comes from the main shell (split). */
|
|
19
|
+
suppressQueryBox?: boolean;
|
|
20
|
+
/** Suppress usage stats line (default: true). */
|
|
21
|
+
suppressUsage?: boolean;
|
|
22
|
+
/** Set interactive-session dynamic context (default: false). */
|
|
23
|
+
interactive?: boolean;
|
|
24
|
+
}
|
|
25
|
+
export interface RemoteSession {
|
|
26
|
+
/** Submit a query to the agent from this session. */
|
|
27
|
+
submit(query: string): void;
|
|
28
|
+
/** The surface this session renders to. */
|
|
29
|
+
readonly surface: import("./utils/compositor.js").RenderSurface;
|
|
30
|
+
/** Whether this session is currently active. */
|
|
31
|
+
readonly active: boolean;
|
|
32
|
+
/** Tear down — restores all routing and advisors. */
|
|
33
|
+
close(): void;
|
|
34
|
+
}
|
|
11
35
|
/** A model entry in the cycling list, optionally tied to a provider. */
|
|
12
36
|
export interface AgentMode {
|
|
13
37
|
model: string;
|
|
@@ -45,8 +69,8 @@ export interface AgentShellConfig {
|
|
|
45
69
|
export interface ExtensionContext {
|
|
46
70
|
bus: EventBus;
|
|
47
71
|
contextManager: ContextManager;
|
|
48
|
-
/**
|
|
49
|
-
|
|
72
|
+
/** Stable per-instance identifier (4-char hex). */
|
|
73
|
+
readonly instanceId: string;
|
|
50
74
|
quit: () => void;
|
|
51
75
|
/** Override color palette slots for theming. */
|
|
52
76
|
setPalette: (overrides: Partial<ColorPalette>) => void;
|
|
@@ -60,12 +84,18 @@ export interface ExtensionContext {
|
|
|
60
84
|
registerCommand: (name: string, description: string, handler: (args: string) => Promise<void> | void) => void;
|
|
61
85
|
/** Register a tool for the built-in agent. No-op when using bridge backends. */
|
|
62
86
|
registerTool: (tool: ToolDefinition) => void;
|
|
87
|
+
/** Unregister a tool by name. */
|
|
88
|
+
unregisterTool: (name: string) => void;
|
|
63
89
|
/** Get all registered tools (for subagent tool subsets). Returns [] when using bridge backends. */
|
|
64
90
|
getTools: () => ToolDefinition[];
|
|
91
|
+
/** Register a named instruction block for the agent's system prompt. */
|
|
92
|
+
registerInstruction: (name: string, text: string) => void;
|
|
93
|
+
/** Remove a named instruction block from the system prompt. */
|
|
94
|
+
removeInstruction: (name: string) => void;
|
|
65
95
|
/** Register a named handler. */
|
|
66
96
|
define: (name: string, fn: (...args: any[]) => any) => void;
|
|
67
|
-
/** Wrap a named handler. Receives `next` (original) + args. */
|
|
68
|
-
advise: (name: string, wrapper: (next: (...args: any[]) => any, ...args: any[]) => any) => void;
|
|
97
|
+
/** Wrap a named handler. Receives `next` (original) + args. Returns an unadvise function. */
|
|
98
|
+
advise: (name: string, wrapper: (next: (...args: any[]) => any, ...args: any[]) => any) => () => void;
|
|
69
99
|
/** Call a named handler. */
|
|
70
100
|
call: (name: string, ...args: any[]) => any;
|
|
71
101
|
/**
|
|
@@ -74,11 +104,20 @@ export interface ExtensionContext {
|
|
|
74
104
|
*/
|
|
75
105
|
terminalBuffer: TerminalBuffer | null;
|
|
76
106
|
/**
|
|
77
|
-
*
|
|
78
|
-
*
|
|
79
|
-
* handler-based customization.
|
|
107
|
+
* Routes named render streams ("agent", "query", "status") to surfaces.
|
|
108
|
+
* Extensions use `compositor.redirect()` to capture output (e.g. overlay panels).
|
|
80
109
|
*/
|
|
81
|
-
|
|
110
|
+
compositor: Compositor;
|
|
111
|
+
/**
|
|
112
|
+
* Create a remote session that routes agent output to a surface and
|
|
113
|
+
* optionally accepts queries. Handles all compositor routing, shell
|
|
114
|
+
* lifecycle advisors, and chrome suppression.
|
|
115
|
+
*
|
|
116
|
+
* const session = ctx.createRemoteSession({ surface, interactive: true });
|
|
117
|
+
* session.submit("what's on screen?");
|
|
118
|
+
* session.close(); // restores everything
|
|
119
|
+
*/
|
|
120
|
+
createRemoteSession: (opts: RemoteSessionOptions) => RemoteSession;
|
|
82
121
|
}
|
|
83
122
|
/**
|
|
84
123
|
* Configuration for a registered input mode.
|
|
@@ -101,12 +140,6 @@ export interface TerminalSession {
|
|
|
101
140
|
done: boolean;
|
|
102
141
|
resolve?: (value: void) => void;
|
|
103
142
|
}
|
|
104
|
-
export interface ToolCallRecord {
|
|
105
|
-
tool: string;
|
|
106
|
-
args: Record<string, unknown>;
|
|
107
|
-
output: string;
|
|
108
|
-
exitCode: number | null;
|
|
109
|
-
}
|
|
110
143
|
export type Exchange = {
|
|
111
144
|
type: "shell_command";
|
|
112
145
|
id: number;
|
|
@@ -124,20 +157,4 @@ export type Exchange = {
|
|
|
124
157
|
id: number;
|
|
125
158
|
timestamp: number;
|
|
126
159
|
query: string;
|
|
127
|
-
} | {
|
|
128
|
-
type: "agent_response";
|
|
129
|
-
id: number;
|
|
130
|
-
timestamp: number;
|
|
131
|
-
response: string;
|
|
132
|
-
toolCalls: ToolCallRecord[];
|
|
133
|
-
} | {
|
|
134
|
-
type: "tool_execution";
|
|
135
|
-
id: number;
|
|
136
|
-
timestamp: number;
|
|
137
|
-
tool: string;
|
|
138
|
-
args: Record<string, unknown>;
|
|
139
|
-
output: string;
|
|
140
|
-
exitCode: number | null;
|
|
141
|
-
outputLines: number;
|
|
142
|
-
outputBytes: number;
|
|
143
160
|
};
|
package/dist/utils/ansi.d.ts
CHANGED
|
@@ -11,5 +11,15 @@ export declare const RESET = "\u001B[0m";
|
|
|
11
11
|
* Excludes SGR (color/style) sequences and accounts for CJK double-width chars.
|
|
12
12
|
*/
|
|
13
13
|
export declare function visibleLen(str: string): number;
|
|
14
|
+
/**
|
|
15
|
+
* Truncate a string to fit within `maxWidth` visible columns.
|
|
16
|
+
* Accounts for CJK double-width characters. Appends `…` if truncated.
|
|
17
|
+
*/
|
|
18
|
+
export declare function truncateToWidth(str: string, maxWidth: number): string;
|
|
19
|
+
/**
|
|
20
|
+
* Pad a string with spaces to fill `targetWidth` visible columns.
|
|
21
|
+
* Accounts for CJK double-width characters.
|
|
22
|
+
*/
|
|
23
|
+
export declare function padEndToWidth(str: string, targetWidth: number): string;
|
|
14
24
|
/** Strip all ANSI escape sequences (SGR, OSC, CSI, private mode) and carriage returns. */
|
|
15
25
|
export declare function stripAnsi(str: string): string;
|
package/dist/utils/ansi.js
CHANGED
|
@@ -70,6 +70,33 @@ export function visibleLen(str) {
|
|
|
70
70
|
}
|
|
71
71
|
return width;
|
|
72
72
|
}
|
|
73
|
+
/**
|
|
74
|
+
* Truncate a string to fit within `maxWidth` visible columns.
|
|
75
|
+
* Accounts for CJK double-width characters. Appends `…` if truncated.
|
|
76
|
+
*/
|
|
77
|
+
export function truncateToWidth(str, maxWidth) {
|
|
78
|
+
const clean = str.replace(/\x1b\[[^m]*m/g, "");
|
|
79
|
+
let width = 0;
|
|
80
|
+
let i = 0;
|
|
81
|
+
for (const char of clean) {
|
|
82
|
+
const cw = charWidth(char.codePointAt(0) ?? 0);
|
|
83
|
+
if (width + cw > maxWidth - 1) {
|
|
84
|
+
// Need room for the "…" (1 column wide)
|
|
85
|
+
return clean.slice(0, i) + "…";
|
|
86
|
+
}
|
|
87
|
+
width += cw;
|
|
88
|
+
i += char.length;
|
|
89
|
+
}
|
|
90
|
+
return clean;
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* Pad a string with spaces to fill `targetWidth` visible columns.
|
|
94
|
+
* Accounts for CJK double-width characters.
|
|
95
|
+
*/
|
|
96
|
+
export function padEndToWidth(str, targetWidth) {
|
|
97
|
+
const gap = targetWidth - visibleLen(str);
|
|
98
|
+
return gap > 0 ? str + " ".repeat(gap) : str;
|
|
99
|
+
}
|
|
73
100
|
/** Strip all ANSI escape sequences (SGR, OSC, CSI, private mode) and carriage returns. */
|
|
74
101
|
export function stripAnsi(str) {
|
|
75
102
|
return str
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Compositor — routes named render streams to surfaces.
|
|
3
|
+
*
|
|
4
|
+
* Components write to named streams ("agent", "query", "status").
|
|
5
|
+
* The compositor decides where each stream actually goes based on
|
|
6
|
+
* the current routing table. Extensions override routing with
|
|
7
|
+
* `redirect()` to capture output (e.g. overlay panels).
|
|
8
|
+
*
|
|
9
|
+
* Streams are hierarchical: "agent:diff" falls back to "agent" if
|
|
10
|
+
* no override or default is registered for "agent:diff" specifically.
|
|
11
|
+
* This enables fine-grained interception — redirect just diffs into
|
|
12
|
+
* a panel, or just a subagent's output ("agent:sub:abc123"), while
|
|
13
|
+
* everything else flows to the parent stream's surface.
|
|
14
|
+
*
|
|
15
|
+
* // tui-renderer registers default surfaces
|
|
16
|
+
* compositor.setDefault("agent", stdoutSurface);
|
|
17
|
+
*
|
|
18
|
+
* // overlay-agent redirects when active
|
|
19
|
+
* const restore = compositor.redirect("agent", panelSurface);
|
|
20
|
+
* // ... later ...
|
|
21
|
+
* restore(); // back to stdout
|
|
22
|
+
*
|
|
23
|
+
* // fine-grained: redirect only diffs to a viewer panel
|
|
24
|
+
* compositor.redirect("agent:diff", diffPanelSurface);
|
|
25
|
+
* // "agent:text", "agent:tool" etc. still go to stdout
|
|
26
|
+
*/
|
|
27
|
+
/**
|
|
28
|
+
* A surface accepts rendered output. Stdout is a surface.
|
|
29
|
+
* A floating panel's content area is a surface. A test buffer is a surface.
|
|
30
|
+
*/
|
|
31
|
+
export interface RenderSurface {
|
|
32
|
+
/** Raw write — supports \r, partial lines, escape codes. */
|
|
33
|
+
write(text: string): void;
|
|
34
|
+
/** Convenience: write + newline. */
|
|
35
|
+
writeLine(line: string): void;
|
|
36
|
+
/** Available width in columns. */
|
|
37
|
+
readonly columns: number;
|
|
38
|
+
}
|
|
39
|
+
export interface Compositor {
|
|
40
|
+
/** Get the currently active surface for a stream. */
|
|
41
|
+
surface(stream: string): RenderSurface;
|
|
42
|
+
/** Override routing: redirect a stream to a different surface.
|
|
43
|
+
* Returns a restore function that undoes the redirect. */
|
|
44
|
+
redirect(stream: string, target: RenderSurface): () => void;
|
|
45
|
+
/** Register the default surface for a stream. */
|
|
46
|
+
setDefault(stream: string, target: RenderSurface): void;
|
|
47
|
+
}
|
|
48
|
+
/** Silent sink — drops all output. Used when no surface is registered. */
|
|
49
|
+
export declare const nullSurface: RenderSurface;
|
|
50
|
+
/** Surface backed by process.stdout. */
|
|
51
|
+
export declare class StdoutSurface implements RenderSurface {
|
|
52
|
+
write(text: string): void;
|
|
53
|
+
writeLine(line: string): void;
|
|
54
|
+
get columns(): number;
|
|
55
|
+
}
|
|
56
|
+
export declare class DefaultCompositor implements Compositor {
|
|
57
|
+
private defaults;
|
|
58
|
+
private overrides;
|
|
59
|
+
surface(stream: string): RenderSurface;
|
|
60
|
+
redirect(stream: string, target: RenderSurface): () => void;
|
|
61
|
+
setDefault(stream: string, target: RenderSurface): void;
|
|
62
|
+
}
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Compositor — routes named render streams to surfaces.
|
|
3
|
+
*
|
|
4
|
+
* Components write to named streams ("agent", "query", "status").
|
|
5
|
+
* The compositor decides where each stream actually goes based on
|
|
6
|
+
* the current routing table. Extensions override routing with
|
|
7
|
+
* `redirect()` to capture output (e.g. overlay panels).
|
|
8
|
+
*
|
|
9
|
+
* Streams are hierarchical: "agent:diff" falls back to "agent" if
|
|
10
|
+
* no override or default is registered for "agent:diff" specifically.
|
|
11
|
+
* This enables fine-grained interception — redirect just diffs into
|
|
12
|
+
* a panel, or just a subagent's output ("agent:sub:abc123"), while
|
|
13
|
+
* everything else flows to the parent stream's surface.
|
|
14
|
+
*
|
|
15
|
+
* // tui-renderer registers default surfaces
|
|
16
|
+
* compositor.setDefault("agent", stdoutSurface);
|
|
17
|
+
*
|
|
18
|
+
* // overlay-agent redirects when active
|
|
19
|
+
* const restore = compositor.redirect("agent", panelSurface);
|
|
20
|
+
* // ... later ...
|
|
21
|
+
* restore(); // back to stdout
|
|
22
|
+
*
|
|
23
|
+
* // fine-grained: redirect only diffs to a viewer panel
|
|
24
|
+
* compositor.redirect("agent:diff", diffPanelSurface);
|
|
25
|
+
* // "agent:text", "agent:tool" etc. still go to stdout
|
|
26
|
+
*/
|
|
27
|
+
/** Silent sink — drops all output. Used when no surface is registered. */
|
|
28
|
+
export const nullSurface = {
|
|
29
|
+
write() { },
|
|
30
|
+
writeLine() { },
|
|
31
|
+
get columns() { return 80; },
|
|
32
|
+
};
|
|
33
|
+
/** Surface backed by process.stdout. */
|
|
34
|
+
export class StdoutSurface {
|
|
35
|
+
write(text) {
|
|
36
|
+
if (process.stdout.writable) {
|
|
37
|
+
try {
|
|
38
|
+
process.stdout.write(text);
|
|
39
|
+
}
|
|
40
|
+
catch { /* ignore */ }
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
writeLine(line) {
|
|
44
|
+
this.write(line + "\n");
|
|
45
|
+
}
|
|
46
|
+
get columns() {
|
|
47
|
+
return process.stdout.columns || 80;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
export class DefaultCompositor {
|
|
51
|
+
defaults = new Map();
|
|
52
|
+
overrides = new Map();
|
|
53
|
+
surface(stream) {
|
|
54
|
+
const stack = this.overrides.get(stream);
|
|
55
|
+
if (stack && stack.length > 0)
|
|
56
|
+
return stack[stack.length - 1];
|
|
57
|
+
if (this.defaults.has(stream))
|
|
58
|
+
return this.defaults.get(stream);
|
|
59
|
+
// Hierarchical fallback: "agent:diff" → "agent"
|
|
60
|
+
const colon = stream.lastIndexOf(":");
|
|
61
|
+
if (colon !== -1)
|
|
62
|
+
return this.surface(stream.slice(0, colon));
|
|
63
|
+
return nullSurface;
|
|
64
|
+
}
|
|
65
|
+
redirect(stream, target) {
|
|
66
|
+
let stack = this.overrides.get(stream);
|
|
67
|
+
if (!stack) {
|
|
68
|
+
stack = [];
|
|
69
|
+
this.overrides.set(stream, stack);
|
|
70
|
+
}
|
|
71
|
+
stack.push(target);
|
|
72
|
+
let restored = false;
|
|
73
|
+
return () => {
|
|
74
|
+
if (restored)
|
|
75
|
+
return;
|
|
76
|
+
restored = true;
|
|
77
|
+
const s = this.overrides.get(stream);
|
|
78
|
+
if (!s)
|
|
79
|
+
return;
|
|
80
|
+
const idx = s.indexOf(target);
|
|
81
|
+
if (idx !== -1)
|
|
82
|
+
s.splice(idx, 1);
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
setDefault(stream, target) {
|
|
86
|
+
this.defaults.set(stream, target);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
@@ -254,7 +254,7 @@ function renderUnified(diff, opts) {
|
|
|
254
254
|
const renderedAsPartOfPair = new Set();
|
|
255
255
|
for (let i = 0; i < hunk.lines.length; i++) {
|
|
256
256
|
const line = hunk.lines[i];
|
|
257
|
-
const no = String(line.oldNo ?? line.newNo ?? "").padStart(noW);
|
|
257
|
+
const no = String(line.type === "removed" ? (line.oldNo ?? "") : (line.newNo ?? line.oldNo ?? "")).padStart(noW);
|
|
258
258
|
if (line.type === "context") {
|
|
259
259
|
const raw = truncateText(line.text, lineTextW);
|
|
260
260
|
const text = lang ? highlightLine(raw, lang) : raw;
|
|
@@ -468,6 +468,91 @@ function truncateText(text, maxWidth) {
|
|
|
468
468
|
}
|
|
469
469
|
return text.slice(0, i) + p.reset + "…";
|
|
470
470
|
}
|
|
471
|
+
// ── Truncation ──────────────────────────────────────────────────
|
|
472
|
+
/**
|
|
473
|
+
* Trim context lines from hunks so the rendered output fits within a budget.
|
|
474
|
+
* Change lines are never removed — only the surrounding context shrinks.
|
|
475
|
+
*/
|
|
476
|
+
function trimHunksToFit(hunks, maxLines) {
|
|
477
|
+
// Count change lines across all hunks
|
|
478
|
+
let changeCount = 0;
|
|
479
|
+
for (const hunk of hunks) {
|
|
480
|
+
for (const line of hunk.lines) {
|
|
481
|
+
if (line.type !== "context")
|
|
482
|
+
changeCount++;
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
// Separators between hunks
|
|
486
|
+
const separators = Math.max(0, hunks.length - 1);
|
|
487
|
+
// How many context lines can we afford?
|
|
488
|
+
const contextBudget = Math.max(0, maxLines - changeCount - separators);
|
|
489
|
+
// Count total context to see if trimming is needed
|
|
490
|
+
let totalContext = 0;
|
|
491
|
+
for (const hunk of hunks) {
|
|
492
|
+
for (const line of hunk.lines) {
|
|
493
|
+
if (line.type === "context")
|
|
494
|
+
totalContext++;
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
if (totalContext <= contextBudget)
|
|
498
|
+
return hunks;
|
|
499
|
+
// Determine how many context lines to keep per side of each change.
|
|
500
|
+
// Binary-search for the largest per-side context that fits.
|
|
501
|
+
let lo = 0;
|
|
502
|
+
let hi = 3; // original context size from groupHunks
|
|
503
|
+
while (lo < hi) {
|
|
504
|
+
const mid = Math.ceil((lo + hi) / 2);
|
|
505
|
+
if (countContextWithLimit(hunks, mid) <= contextBudget)
|
|
506
|
+
lo = mid;
|
|
507
|
+
else
|
|
508
|
+
hi = mid - 1;
|
|
509
|
+
}
|
|
510
|
+
return rebuildHunks(hunks, lo);
|
|
511
|
+
}
|
|
512
|
+
/** Count how many context lines remain if we keep at most `ctx` per side of each change. */
|
|
513
|
+
function countContextWithLimit(hunks, ctx) {
|
|
514
|
+
let count = 0;
|
|
515
|
+
for (const hunk of hunks) {
|
|
516
|
+
const lines = hunk.lines;
|
|
517
|
+
for (let i = 0; i < lines.length; i++) {
|
|
518
|
+
if (lines[i].type !== "context")
|
|
519
|
+
continue;
|
|
520
|
+
// Keep this context line if it's within `ctx` of any change
|
|
521
|
+
let nearChange = false;
|
|
522
|
+
for (let d = 1; d <= ctx; d++) {
|
|
523
|
+
if ((i - d >= 0 && lines[i - d].type !== "context") ||
|
|
524
|
+
(i + d < lines.length && lines[i + d].type !== "context")) {
|
|
525
|
+
nearChange = true;
|
|
526
|
+
break;
|
|
527
|
+
}
|
|
528
|
+
}
|
|
529
|
+
if (nearChange)
|
|
530
|
+
count++;
|
|
531
|
+
}
|
|
532
|
+
}
|
|
533
|
+
return count;
|
|
534
|
+
}
|
|
535
|
+
/** Rebuild hunks keeping only context lines within `ctx` distance of a change. */
|
|
536
|
+
function rebuildHunks(hunks, ctx) {
|
|
537
|
+
return hunks.map((hunk) => {
|
|
538
|
+
const lines = hunk.lines;
|
|
539
|
+
const kept = [];
|
|
540
|
+
for (let i = 0; i < lines.length; i++) {
|
|
541
|
+
if (lines[i].type !== "context") {
|
|
542
|
+
kept.push(lines[i]);
|
|
543
|
+
continue;
|
|
544
|
+
}
|
|
545
|
+
for (let d = 1; d <= ctx; d++) {
|
|
546
|
+
if ((i - d >= 0 && lines[i - d].type !== "context") ||
|
|
547
|
+
(i + d < lines.length && lines[i + d].type !== "context")) {
|
|
548
|
+
kept.push(lines[i]);
|
|
549
|
+
break;
|
|
550
|
+
}
|
|
551
|
+
}
|
|
552
|
+
}
|
|
553
|
+
return { lines: kept };
|
|
554
|
+
});
|
|
555
|
+
}
|
|
471
556
|
// ── Public API ───────────────────────────────────────────────────
|
|
472
557
|
/** Select display mode based on available terminal width. */
|
|
473
558
|
export function selectMode(width) {
|
|
@@ -487,16 +572,19 @@ export function renderDiff(diff, opts) {
|
|
|
487
572
|
if (mode === "summary") {
|
|
488
573
|
return [header, ...renderSummary(diff)];
|
|
489
574
|
}
|
|
575
|
+
// Trim context lines from hunks if the diff would exceed the budget,
|
|
576
|
+
// so that actual changes are always visible.
|
|
577
|
+
const trimmed = { ...diff, hunks: trimHunksToFit(diff.hunks, maxLines) };
|
|
490
578
|
let bodyLines;
|
|
491
579
|
switch (mode) {
|
|
492
580
|
case "split":
|
|
493
|
-
bodyLines = renderSplit(
|
|
581
|
+
bodyLines = renderSplit(trimmed, opts);
|
|
494
582
|
break;
|
|
495
583
|
case "unified":
|
|
496
|
-
bodyLines = renderUnified(
|
|
584
|
+
bodyLines = renderUnified(trimmed, opts);
|
|
497
585
|
break;
|
|
498
586
|
}
|
|
499
|
-
//
|
|
587
|
+
// Final safety net — if still over budget, simple tail truncation.
|
|
500
588
|
if (bodyLines.length > maxLines) {
|
|
501
589
|
const overflow = bodyLines.length - maxLines;
|
|
502
590
|
bodyLines = bodyLines.slice(0, maxLines);
|
|
@@ -20,7 +20,7 @@ export interface FloatingPanelConfig {
|
|
|
20
20
|
* Requires @xterm/headless — falls back to blank background if unavailable.
|
|
21
21
|
*/
|
|
22
22
|
dimBackground?: boolean;
|
|
23
|
-
/** Auto-dismiss delay in ms when done (0 =
|
|
23
|
+
/** Auto-dismiss delay in ms when done (0 = auto-prompt for follow-up). Default: 0. */
|
|
24
24
|
autoDismissMs?: number;
|
|
25
25
|
/** Icon shown before the input cursor. Default: "\u276f". */
|
|
26
26
|
promptIcon?: string;
|
|
@@ -144,6 +144,7 @@ export declare class FloatingPanel {
|
|
|
144
144
|
* - `{prefix}:composite-row(content: string, bgLine: string|null, boxLeft: number, boxW: number, cols: number) -> string`
|
|
145
145
|
* - `{prefix}:submit(query: string) -> void`
|
|
146
146
|
* - `{prefix}:dismiss() -> void`
|
|
147
|
+
* - `{prefix}:show() -> void`
|
|
147
148
|
* - `{prefix}:input(data: string) -> boolean`
|
|
148
149
|
* - `{prefix}:build-row(content: string, width: number) -> string`
|
|
149
150
|
*/
|
|
@@ -153,29 +154,49 @@ export declare class FloatingPanel {
|
|
|
153
154
|
/** All byte sequences that should be recognized as the trigger key. */
|
|
154
155
|
private readonly triggerSeqs;
|
|
155
156
|
private phase;
|
|
157
|
+
private _visible;
|
|
158
|
+
private _passthrough;
|
|
156
159
|
private editor;
|
|
157
160
|
private contentLines;
|
|
158
161
|
private currentPartialLine;
|
|
159
162
|
private scrollOffset;
|
|
163
|
+
private userScrolled;
|
|
160
164
|
private title;
|
|
161
165
|
private footer;
|
|
162
166
|
private renderTimer;
|
|
163
|
-
private autoDismissTimer;
|
|
164
167
|
private resizeHandler;
|
|
165
168
|
private prevFrame;
|
|
166
169
|
private suppressNextRedraw;
|
|
170
|
+
private autoDismissTimer;
|
|
167
171
|
private ptyBuffer;
|
|
168
172
|
private usedAltScreen;
|
|
173
|
+
private wrapCache;
|
|
174
|
+
private wrapCacheWidth;
|
|
175
|
+
private passthroughTimer;
|
|
176
|
+
private prevSerialized;
|
|
169
177
|
constructor(bus: EventBus, config: FloatingPanelConfig, handlers?: HandlerRegistry);
|
|
170
178
|
private registerDefaultHandlers;
|
|
171
179
|
private wireEvents;
|
|
172
180
|
/** Check whether data matches any encoding of the trigger key. */
|
|
173
181
|
private isTrigger;
|
|
174
182
|
private ensureBuffer;
|
|
183
|
+
/** Whether the panel has an active conversation (may be hidden). */
|
|
175
184
|
get active(): boolean;
|
|
185
|
+
/** Whether the agent is currently processing a query. */
|
|
186
|
+
get processing(): boolean;
|
|
187
|
+
/** Whether the panel is currently visible on screen. */
|
|
188
|
+
get visible(): boolean;
|
|
176
189
|
get terminalBuffer(): TerminalBuffer | null;
|
|
190
|
+
/** Open a fresh panel with a new conversation. */
|
|
177
191
|
open(): void;
|
|
192
|
+
/** Hide the panel without destroying conversation state. */
|
|
193
|
+
hide(): void;
|
|
194
|
+
/** Show the panel again after hide(), preserving conversation. */
|
|
195
|
+
show(): void;
|
|
196
|
+
/** Fully destroy the panel, resetting all state. */
|
|
178
197
|
dismiss(): void;
|
|
198
|
+
/** Common screen enter logic shared by open() and show(). */
|
|
199
|
+
private enterScreen;
|
|
179
200
|
appendText(text: string): void;
|
|
180
201
|
appendLine(line: string): void;
|
|
181
202
|
updateLastLine(fn: (line: string) => string): void;
|
|
@@ -184,15 +205,25 @@ export declare class FloatingPanel {
|
|
|
184
205
|
setFooter(footer: string): void;
|
|
185
206
|
setActive(): void;
|
|
186
207
|
setDone(): void;
|
|
208
|
+
scrollUp(lines?: number): void;
|
|
209
|
+
scrollDown(lines?: number): void;
|
|
187
210
|
getInput(): string;
|
|
188
211
|
requestRender(): void;
|
|
189
212
|
private handleIntercept;
|
|
213
|
+
/** Handle scroll input. Returns true if consumed. */
|
|
214
|
+
private handleScroll;
|
|
190
215
|
private handleInputKey;
|
|
191
216
|
/** Compute box geometry from config + current terminal size. */
|
|
192
217
|
computeGeometry(): BoxGeometry;
|
|
193
218
|
private buildFrame;
|
|
194
219
|
private scheduleRender;
|
|
195
220
|
private render;
|
|
196
|
-
|
|
221
|
+
/** Full screen teardown: exit alt screen, release stdout, force redraw. */
|
|
222
|
+
private teardownScreen;
|
|
223
|
+
/** Start rendering TerminalBuffer directly (no overlay box). */
|
|
224
|
+
private startPassthrough;
|
|
225
|
+
private stopPassthrough;
|
|
226
|
+
/** Render the TerminalBuffer's screen content directly (no overlay). */
|
|
227
|
+
private renderPassthrough;
|
|
197
228
|
private resolveSize;
|
|
198
229
|
}
|