agent-sh 0.14.10 → 0.15.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 +36 -13
- package/dist/agent/agent-loop.d.ts +9 -17
- package/dist/agent/agent-loop.js +123 -150
- package/dist/agent/events.d.ts +10 -12
- package/dist/agent/host-types.d.ts +17 -11
- package/dist/agent/index.d.ts +1 -1
- package/dist/agent/index.js +76 -29
- package/dist/agent/live-view.d.ts +3 -3
- package/dist/agent/live-view.js +15 -7
- package/dist/agent/providers/deepseek.js +9 -1
- package/dist/agent/providers/openrouter.js +9 -0
- package/dist/agent/session-store.js +1 -1
- package/dist/agent/subagent.js +1 -1
- package/dist/agent/system-prompt.d.ts +7 -3
- package/dist/agent/system-prompt.js +11 -14
- package/dist/agent/tool-protocol.js +0 -7
- package/dist/cli/args.js +2 -1
- package/dist/cli/install.d.ts +1 -0
- package/dist/cli/install.js +39 -2
- package/dist/cli/subcommands.js +1 -0
- package/dist/core/event-bus.js +0 -2
- package/dist/core/extension-loader.js +3 -1
- package/dist/core/index.d.ts +1 -1
- package/dist/core/index.js +3 -2
- package/dist/extensions/slash-commands/index.js +16 -11
- package/dist/shell/events.d.ts +3 -0
- package/dist/shell/index.js +9 -0
- package/dist/shell/shell-context.d.ts +2 -2
- package/dist/shell/shell-context.js +26 -11
- package/dist/shell/shell.js +3 -0
- package/dist/shell/tui-renderer.js +0 -1
- package/dist/utils/diff-renderer.d.ts +4 -0
- package/dist/utils/diff-renderer.js +15 -27
- package/dist/utils/handler-registry.d.ts +1 -6
- package/dist/utils/handler-registry.js +1 -6
- package/dist/utils/line-editor.js +0 -2
- package/dist/utils/palette.js +4 -4
- package/dist/utils/terminal-buffer.d.ts +2 -0
- package/dist/utils/terminal-buffer.js +4 -0
- package/examples/extensions/ads/SKILL.md +170 -0
- package/examples/extensions/ash-acp-bridge/src/index.ts +11 -7
- package/examples/extensions/ash-scheme/index.ts +377 -687
- package/examples/extensions/ash-scheme/package.json +1 -1
- package/examples/extensions/ashi/EXTENDING.md +118 -0
- package/examples/extensions/ashi/README.md +26 -54
- package/examples/extensions/ashi/docs/ui-surface-protocol.md +163 -0
- package/examples/extensions/ashi/package.json +14 -2
- package/examples/extensions/ashi/src/autocomplete-controller.ts +95 -0
- package/examples/extensions/ashi/src/autocomplete.ts +1 -23
- package/examples/extensions/ashi/src/capture.ts +54 -10
- package/examples/extensions/ashi/src/chat/assistant.ts +67 -0
- package/examples/extensions/ashi/src/chat/lines.ts +39 -0
- package/examples/extensions/ashi/src/chat/thinking.ts +42 -0
- package/examples/extensions/ashi/src/chat/tool-group.ts +84 -0
- package/examples/extensions/ashi/src/chat/user-message.ts +20 -0
- package/examples/extensions/ashi/src/cli.ts +80 -12
- package/examples/extensions/ashi/src/clipboard-image.ts +41 -0
- package/examples/extensions/ashi/src/commands.ts +11 -1
- package/examples/extensions/ashi/src/dialogs.ts +67 -0
- package/examples/extensions/ashi/src/display-config.ts +16 -1
- package/examples/extensions/ashi/src/docks.ts +31 -0
- package/examples/extensions/ashi/src/events.ts +16 -0
- package/examples/extensions/ashi/src/frontend.ts +456 -268
- package/examples/extensions/ashi/src/hooks.ts +27 -40
- package/examples/extensions/ashi/src/input-prompt.ts +64 -0
- package/examples/extensions/ashi/src/renderer.ts +222 -0
- package/examples/extensions/ashi/src/renderers/pi-tui/app.ts +122 -0
- package/examples/extensions/ashi/src/renderers/pi-tui/index.ts +27 -0
- package/examples/extensions/ashi/src/renderers/pi-tui/nodes.ts +190 -0
- package/examples/extensions/ashi/src/renderers/pi-tui/schema-mount.ts +203 -0
- package/examples/extensions/ashi/src/renderers/pi-tui/theme-adapters.ts +48 -0
- package/examples/extensions/ashi/src/renderers/pi-tui/tool-group.ts +21 -0
- package/examples/extensions/ashi/src/schema.ts +46 -205
- package/examples/extensions/ashi/src/session-commands.ts +2 -1
- package/examples/extensions/ashi/src/status-footer.ts +35 -25
- package/examples/extensions/ashi/src/terminal-mode.ts +9 -0
- package/examples/extensions/ashi/src/theme.ts +1 -47
- package/examples/extensions/ashi/src/ui.ts +88 -0
- package/examples/extensions/ashi-ink/README.md +61 -0
- package/examples/extensions/ashi-ink/package.json +30 -0
- package/examples/extensions/ashi-ink/src/index.ts +6 -0
- package/examples/extensions/ashi-ink/src/ink-renderer.tsx +865 -0
- package/examples/extensions/ashi-ink/src/shims.d.ts +5 -0
- package/examples/extensions/ashi-ink/test/render.test.tsx +408 -0
- package/examples/extensions/ashi-ink/tsconfig.json +14 -0
- package/examples/extensions/ashi-scheme-render.ts +10 -10
- package/examples/extensions/ashi-shell-passthrough.ts +95 -0
- package/examples/extensions/ashi-ui-demo.ts +63 -0
- package/examples/extensions/latex-images.ts +70 -19
- package/examples/extensions/overlay-agent.ts +5 -5
- package/examples/extensions/pi-bridge/index.ts +7 -12
- package/examples/extensions/terminal-buffer.ts +4 -2
- package/package.json +3 -9
- package/examples/extensions/ashi/src/components.ts +0 -238
- package/examples/extensions/ollama.ts +0 -108
- package/examples/extensions/opencode-provider.ts +0 -251
- package/examples/extensions/zai-coding-plan.ts +0 -35
package/dist/cli/install.js
CHANGED
|
@@ -14,7 +14,16 @@ const EXT_DIR = path.join(CONFIG_DIR, "extensions");
|
|
|
14
14
|
export function listBundled() {
|
|
15
15
|
if (!fs.existsSync(BUNDLED_DIR))
|
|
16
16
|
return [];
|
|
17
|
-
|
|
17
|
+
const out = [];
|
|
18
|
+
for (const d of fs.readdirSync(BUNDLED_DIR, { withFileTypes: true })) {
|
|
19
|
+
if (d.name.startsWith("."))
|
|
20
|
+
continue;
|
|
21
|
+
if (d.isDirectory())
|
|
22
|
+
out.push(d.name);
|
|
23
|
+
else if (SCRIPT_EXTS.some((ext) => d.name.endsWith(ext)))
|
|
24
|
+
out.push(d.name.replace(/\.[^.]+$/, ""));
|
|
25
|
+
}
|
|
26
|
+
return out;
|
|
18
27
|
}
|
|
19
28
|
/** Heuristic: a backend named "pi" is typically provided by an extension called "pi-bridge". */
|
|
20
29
|
export function suggestBridgeFor(backend) {
|
|
@@ -181,6 +190,32 @@ function rewriteFileDeps(target, sourcePath) {
|
|
|
181
190
|
if (changed)
|
|
182
191
|
fs.writeFileSync(pkgJson, `${JSON.stringify(pkg, null, 2)}\n`);
|
|
183
192
|
}
|
|
193
|
+
/** --dev: repoint the extension's agent-sh dep at the running host's package
|
|
194
|
+
* root, so the install builds and runs against the local (possibly unreleased)
|
|
195
|
+
* core instead of the published registry version. npm links the file: path, so
|
|
196
|
+
* later core rebuilds flow through without reinstalling. */
|
|
197
|
+
function pinHostCore(target) {
|
|
198
|
+
const pkgJson = path.join(target, "package.json");
|
|
199
|
+
if (!fs.existsSync(pkgJson))
|
|
200
|
+
return;
|
|
201
|
+
const pkg = JSON.parse(fs.readFileSync(pkgJson, "utf-8"));
|
|
202
|
+
const sections = ["dependencies", "devDependencies", "peerDependencies", "optionalDependencies"];
|
|
203
|
+
let changed = false;
|
|
204
|
+
for (const section of sections) {
|
|
205
|
+
const deps = pkg[section];
|
|
206
|
+
if (!deps || typeof deps !== "object")
|
|
207
|
+
continue;
|
|
208
|
+
const d = deps;
|
|
209
|
+
if (typeof d["agent-sh"] !== "string")
|
|
210
|
+
continue;
|
|
211
|
+
d["agent-sh"] = `file:${PACKAGE_ROOT}`;
|
|
212
|
+
changed = true;
|
|
213
|
+
}
|
|
214
|
+
if (!changed)
|
|
215
|
+
return;
|
|
216
|
+
fs.writeFileSync(pkgJson, `${JSON.stringify(pkg, null, 2)}\n`);
|
|
217
|
+
console.log(`agent-sh: --dev — linking ${path.basename(target)} against host core at ${PACKAGE_ROOT}`);
|
|
218
|
+
}
|
|
184
219
|
function maybeNpmInstall(target, pkg) {
|
|
185
220
|
const deps = { ...(pkg.dependencies ?? {}), ...(pkg.peerDependencies ?? {}) };
|
|
186
221
|
if (Object.keys(deps).length === 0)
|
|
@@ -243,7 +278,7 @@ function linkBins(target, pkg) {
|
|
|
243
278
|
}
|
|
244
279
|
export async function runInstall(spec, opts = {}) {
|
|
245
280
|
if (!spec) {
|
|
246
|
-
console.error("Usage: agent-sh install <name|file:|npm:|github:> [--force] [--sync-deps]\n\n" +
|
|
281
|
+
console.error("Usage: agent-sh install <name|file:|npm:|github:> [--force] [--sync-deps] [--dev]\n\n" +
|
|
247
282
|
"Bundled extensions:\n" +
|
|
248
283
|
listBundled()
|
|
249
284
|
.map((n) => ` ${n}`)
|
|
@@ -277,6 +312,8 @@ export async function runInstall(spec, opts = {}) {
|
|
|
277
312
|
});
|
|
278
313
|
try {
|
|
279
314
|
rewriteFileDeps(target, resolved.sourcePath);
|
|
315
|
+
if (opts.dev)
|
|
316
|
+
pinHostCore(target);
|
|
280
317
|
syncAgentShVersion(target, opts.syncDeps ?? false);
|
|
281
318
|
const pkg = readPackageJson(target);
|
|
282
319
|
if (pkg) {
|
package/dist/cli/subcommands.js
CHANGED
package/dist/core/event-bus.js
CHANGED
|
@@ -159,9 +159,7 @@ export class EventBus {
|
|
|
159
159
|
* returns the original payload unchanged (with safe defaults).
|
|
160
160
|
*/
|
|
161
161
|
async emitPipeAsync(event, payload) {
|
|
162
|
-
// Phase 1: notify (lets renderers prepare for interactive I/O)
|
|
163
162
|
this.dispatch(event, payload);
|
|
164
|
-
// Phase 2: transform (extensions provide decisions)
|
|
165
163
|
const listeners = this.asyncPipeListeners.get(event);
|
|
166
164
|
if (!listeners)
|
|
167
165
|
return payload;
|
|
@@ -119,7 +119,9 @@ function createScopedContext(ctx, extensionName) {
|
|
|
119
119
|
onDispose: (fn) => { cleanups.push(fn); },
|
|
120
120
|
};
|
|
121
121
|
const dispose = () => {
|
|
122
|
-
|
|
122
|
+
// Snapshot: a re-registering cleanup appends a new cleanup, and iterating
|
|
123
|
+
// the live array would run it and undo the restore in the same pass.
|
|
124
|
+
for (const fn of cleanups.slice()) {
|
|
123
125
|
try {
|
|
124
126
|
fn();
|
|
125
127
|
}
|
package/dist/core/index.d.ts
CHANGED
|
@@ -13,7 +13,7 @@ import { HandlerRegistry } from "../utils/handler-registry.js";
|
|
|
13
13
|
export { EventBus } from "./event-bus.js";
|
|
14
14
|
export type { BusEvents, ContentBlock, BackendRegistration } from "./event-bus.js";
|
|
15
15
|
export type { CoreContext, CoreConfig } from "./types.js";
|
|
16
|
-
export type { AgentContext, AgentConfig, AgentSurface, AgentConfigSurface,
|
|
16
|
+
export type { AgentContext, AgentConfig, AgentSurface, AgentConfigSurface, Model, LlmInterface, LlmMessage, LlmSession } from "../agent/host-types.js";
|
|
17
17
|
export type { ShellContext, ShellConfig, ShellSurface, ShellConfigSurface, ExtensionContext, RemoteSession, RemoteSessionOptions, RenderSurface, InputModeConfig, TerminalSession, BlockTransformOptions, FencedBlockTransformOptions, AppConfig } from "../shell/host-types.js";
|
|
18
18
|
export { palette, setPalette, resetPalette } from "../utils/palette.js";
|
|
19
19
|
export type { ColorPalette } from "../utils/palette.js";
|
package/dist/core/index.js
CHANGED
|
@@ -29,10 +29,11 @@ export function createCore(config) {
|
|
|
29
29
|
bus.setSource(instanceId);
|
|
30
30
|
handlers.define("config:get-app-config", () => config);
|
|
31
31
|
handlers.define("cwd", () => process.cwd());
|
|
32
|
-
// Empty defaults so
|
|
33
|
-
//
|
|
32
|
+
// Empty defaults so advisors can wrap these regardless of load order;
|
|
33
|
+
// system-prompt:frontend is where the active frontend describes its surface.
|
|
34
34
|
handlers.define("dynamic-context:build", () => "");
|
|
35
35
|
handlers.define("query-context:build", () => "");
|
|
36
|
+
handlers.define("system-prompt:frontend", () => "");
|
|
36
37
|
const backends = new Map();
|
|
37
38
|
let activeBackendName = null;
|
|
38
39
|
bus.on("agent:register-backend", (backend) => {
|
|
@@ -41,16 +41,21 @@ export default function activate(ctx) {
|
|
|
41
41
|
description: "Cycle to next model, or switch to a specific one",
|
|
42
42
|
handler: (args) => {
|
|
43
43
|
const name = args.trim();
|
|
44
|
+
const { models, active } = bus.emitPipe("config:get-models", { models: [], active: null });
|
|
44
45
|
if (!name) {
|
|
45
|
-
const
|
|
46
|
-
const label = active
|
|
47
|
-
? `${active.model}${active.provider ? ` [${active.provider}]` : ""}`
|
|
48
|
-
: "none";
|
|
46
|
+
const label = active ? `${active.id} [${active.provider}]` : "none";
|
|
49
47
|
bus.emit("ui:info", { message: `Model: ${label}` });
|
|
48
|
+
return;
|
|
50
49
|
}
|
|
51
|
-
|
|
52
|
-
|
|
50
|
+
const atIdx = name.lastIndexOf("@");
|
|
51
|
+
const id = atIdx > 0 ? name.slice(0, atIdx) : name;
|
|
52
|
+
const providerHint = atIdx > 0 ? name.slice(atIdx + 1) : undefined;
|
|
53
|
+
const found = models.find((m) => m.id === id && (!providerHint || m.provider === providerHint));
|
|
54
|
+
if (!found) {
|
|
55
|
+
bus.emit("ui:error", { message: `Unknown model: ${name}` });
|
|
56
|
+
return;
|
|
53
57
|
}
|
|
58
|
+
bus.emit("config:switch-model", { id: found.id, provider: found.provider });
|
|
54
59
|
},
|
|
55
60
|
});
|
|
56
61
|
register({
|
|
@@ -163,16 +168,16 @@ export default function activate(ctx) {
|
|
|
163
168
|
const { models, active } = bus.emitPipe("config:get-models", { models: [], active: null });
|
|
164
169
|
const counts = new Map();
|
|
165
170
|
for (const m of models)
|
|
166
|
-
counts.set(m.
|
|
171
|
+
counts.set(m.id, (counts.get(m.id) ?? 0) + 1);
|
|
167
172
|
const items = models
|
|
168
|
-
.filter((m) => m.
|
|
173
|
+
.filter((m) => m.id.toLowerCase().includes(partial))
|
|
169
174
|
.slice(0, 15)
|
|
170
175
|
.map((m) => {
|
|
171
|
-
const ambiguous = (counts.get(m.
|
|
172
|
-
const qualified = ambiguous ? `${m.
|
|
176
|
+
const ambiguous = (counts.get(m.id) ?? 0) > 1;
|
|
177
|
+
const qualified = ambiguous ? `${m.id}@${m.provider}` : m.id;
|
|
173
178
|
return {
|
|
174
179
|
name: `/model ${qualified}`,
|
|
175
|
-
description:
|
|
180
|
+
description: `[${m.provider}]${active && m.id === active.id && m.provider === active.provider ? " (active)" : ""}`,
|
|
176
181
|
};
|
|
177
182
|
});
|
|
178
183
|
if (items.length === 0)
|
package/dist/shell/events.d.ts
CHANGED
package/dist/shell/index.js
CHANGED
|
@@ -7,11 +7,13 @@ import "./events.js"; // augments BusEvents with shell-owned events
|
|
|
7
7
|
import { Shell } from "./shell.js";
|
|
8
8
|
import { DefaultCompositor } from "../utils/compositor.js";
|
|
9
9
|
import { TerminalBuffer } from "../utils/terminal-buffer.js";
|
|
10
|
+
import { FloatingPanel } from "../utils/floating-panel.js";
|
|
10
11
|
import { setPalette } from "../utils/palette.js";
|
|
11
12
|
import * as streamTransform from "../utils/stream-transform.js";
|
|
12
13
|
import activateShellContext from "./shell-context.js";
|
|
13
14
|
import activateTuiRenderer from "./tui-renderer.js";
|
|
14
15
|
import { processTerminal, surfaceFromTerminal } from "./terminal.js";
|
|
16
|
+
const SHELL_SURFACE = `You're attached through a terminal shell. It shares the user's working directory, environment, and command history, and you can act on their live session — everything they run at the prompt is visible to you.`;
|
|
15
17
|
/**
|
|
16
18
|
* Register shell-owned handlers extensions can `ctx.call`, and attach
|
|
17
19
|
* the shell surface to ctx. Must run before `loadExtensions` so user
|
|
@@ -20,6 +22,10 @@ import { processTerminal, surfaceFromTerminal } from "./terminal.js";
|
|
|
20
22
|
export function registerShellHandlers(ctx) {
|
|
21
23
|
const { bus } = ctx;
|
|
22
24
|
const compositor = new DefaultCompositor(bus);
|
|
25
|
+
ctx.advise("system-prompt:frontend", (next) => {
|
|
26
|
+
const base = next() ?? "";
|
|
27
|
+
return base ? `${base}\n\n${SHELL_SURFACE}` : SHELL_SURFACE;
|
|
28
|
+
});
|
|
23
29
|
const shellSurface = {
|
|
24
30
|
compositor,
|
|
25
31
|
setPalette,
|
|
@@ -69,6 +75,9 @@ export function registerShellHandlers(ctx) {
|
|
|
69
75
|
terminalBufferSingleton = TerminalBuffer.createWired(ctx.bus);
|
|
70
76
|
return terminalBufferSingleton;
|
|
71
77
|
});
|
|
78
|
+
// bus override lets callers pass their scoped bus, so the panel's
|
|
79
|
+
// listeners unwire when the extension reloads.
|
|
80
|
+
ctx.define("floating-panel:create", (config, bus) => new FloatingPanel(bus ?? ctx.bus, config));
|
|
72
81
|
activateShellContext(ctx);
|
|
73
82
|
activateTuiRenderer(ctx);
|
|
74
83
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/** Tracks PTY commands and cwd, spills long outputs, contributes per-query
|
|
2
|
-
* `<
|
|
3
|
-
* without a PTY skip this
|
|
2
|
+
* `<shell_events>` (fresh user exchanges) and — under the shell frontend —
|
|
3
|
+
* `<cwd>`. Frontends without a PTY skip this. */
|
|
4
4
|
import type { ExtensionContext } from "./host-types.js";
|
|
5
5
|
export default function activate(ctx: ExtensionContext): void;
|
|
@@ -1,7 +1,15 @@
|
|
|
1
1
|
import { getSettings } from "../core/settings.js";
|
|
2
2
|
import { spillOutput } from "../utils/shell-output-spill.js";
|
|
3
|
+
// The cwd-drift note applies only under the shell frontend (where the agent shares
|
|
4
|
+
// the user's cwd); other frontends own a fixed cwd.
|
|
5
|
+
const SHELL_EVENTS_NOTE = `When the user runs shell commands, they appear as \`<shell_events>\` inside \`<query_context>\` on your next turn — use them to ground "fix this" / "what just happened" requests.`;
|
|
6
|
+
const CWD_DRIFT_NOTE = `\`<cwd>\` is the working directory your own tool calls run in: relative paths resolve against it, and it follows the user's shell \`cd\`, so it can change from one turn to the next. Always act on the latest \`<cwd>\`, not one from earlier in the conversation.`;
|
|
7
|
+
const PREFERENCES_NOTE = `Treat the user's commands as standing preferences: check them for recurring patterns and apply them proactively, without waiting to be asked.`;
|
|
3
8
|
export default function activate(ctx) {
|
|
4
9
|
const { bus } = ctx;
|
|
10
|
+
// The agent shares the user's cwd only under the shell frontend (which installs
|
|
11
|
+
// ctx.shell); other frontends — e.g. ashi — keep their own fixed cwd.
|
|
12
|
+
const ownsAgentCwd = !!ctx.shell;
|
|
5
13
|
const exchanges = [];
|
|
6
14
|
let nextId = 1;
|
|
7
15
|
let currentCwd = process.cwd();
|
|
@@ -50,23 +58,30 @@ export default function activate(ctx) {
|
|
|
50
58
|
bus.on("shell:agent-exec-start", () => { agentShellActive = true; });
|
|
51
59
|
bus.on("shell:agent-exec-done", () => { agentShellActive = false; });
|
|
52
60
|
bus.on("shell:user-exec-exclude-next", () => { nextUserExcluded = true; });
|
|
53
|
-
|
|
61
|
+
if (ownsAgentCwd)
|
|
62
|
+
ctx.advise("cwd", () => currentCwd);
|
|
54
63
|
// Advise the core handler directly: this loads before the agent host
|
|
55
64
|
// attaches `ctx.agent`, so the sugar isn't available yet.
|
|
56
65
|
ctx.advise("query-context:build", (next) => {
|
|
57
66
|
const base = next();
|
|
58
|
-
const
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
if (fresh.length === 0)
|
|
62
|
-
return cwdTag;
|
|
67
|
+
const fresh = exchanges.filter((ex) => ex.id > lastSeq && ex.source === "user");
|
|
68
|
+
let shellEvents = "";
|
|
69
|
+
if (fresh.length > 0) {
|
|
63
70
|
lastSeq = exchanges[exchanges.length - 1].id;
|
|
64
71
|
const text = fresh.map(formatExchangeTruncated).filter(Boolean).join("\n");
|
|
65
|
-
if (
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
72
|
+
if (text)
|
|
73
|
+
shellEvents = `<shell_events>\n${text}\n</shell_events>`;
|
|
74
|
+
}
|
|
75
|
+
const part = ownsAgentCwd
|
|
76
|
+
? [`<cwd>${currentCwd}</cwd>`, shellEvents].filter(Boolean).join("\n")
|
|
77
|
+
: shellEvents;
|
|
78
|
+
return [base, part].filter(Boolean).join("\n\n");
|
|
79
|
+
});
|
|
80
|
+
bus.onPipe("agent:instructions", (acc) => {
|
|
81
|
+
const text = [SHELL_EVENTS_NOTE, ownsAgentCwd ? CWD_DRIFT_NOTE : "", PREFERENCES_NOTE]
|
|
82
|
+
.filter(Boolean).join("\n\n");
|
|
83
|
+
acc.instructions.push({ name: "shell-events", text });
|
|
84
|
+
return acc;
|
|
70
85
|
});
|
|
71
86
|
ctx.define("shell:context-recent", (n = 25) => {
|
|
72
87
|
const recent = exchanges.slice(-n);
|
package/dist/shell/shell.js
CHANGED
|
@@ -110,6 +110,9 @@ export class Shell {
|
|
|
110
110
|
this.bus.on("shell:pty-resize", ({ cols, rows }) => {
|
|
111
111
|
this.ptyProcess.resize(cols, rows);
|
|
112
112
|
});
|
|
113
|
+
this.bus.on("shell:host-write", ({ data }) => {
|
|
114
|
+
this.terminal.write(data);
|
|
115
|
+
});
|
|
113
116
|
// Compat shims for the bus-event API. shell:stdout-hold maps to hard
|
|
114
117
|
// mute so terminal_keys' stdout-show can't paint through the overlay.
|
|
115
118
|
let holdRefcount = 0;
|
|
@@ -13,6 +13,10 @@ export interface DiffRenderOptions {
|
|
|
13
13
|
trueColor?: boolean;
|
|
14
14
|
/** Enable syntax highlighting on diff lines. Default true. */
|
|
15
15
|
syntaxHighlight?: boolean;
|
|
16
|
+
/** Draw the `│` rule between the line number and the code (default true). Set false
|
|
17
|
+
* for a flush gutter: `<n> <sigil><code>`, the row background spans the line, and
|
|
18
|
+
* context code is left un-dimmed. */
|
|
19
|
+
gutterLine?: boolean;
|
|
16
20
|
}
|
|
17
21
|
export declare function detectLanguage(filePath?: string): string | undefined;
|
|
18
22
|
/**
|
|
@@ -176,17 +176,14 @@ function findChangePairs(hunk) {
|
|
|
176
176
|
const lines = hunk.lines;
|
|
177
177
|
let i = 0;
|
|
178
178
|
while (i < lines.length) {
|
|
179
|
-
// Find a run of removed lines
|
|
180
179
|
const removedStart = i;
|
|
181
180
|
while (i < lines.length && lines[i].type === "removed")
|
|
182
181
|
i++;
|
|
183
182
|
const removedEnd = i;
|
|
184
|
-
// Find a run of added lines immediately after
|
|
185
183
|
const addedStart = i;
|
|
186
184
|
while (i < lines.length && lines[i].type === "added")
|
|
187
185
|
i++;
|
|
188
186
|
const addedEnd = i;
|
|
189
|
-
// Pair them 1:1
|
|
190
187
|
const removedCount = removedEnd - removedStart;
|
|
191
188
|
const addedCount = addedEnd - addedStart;
|
|
192
189
|
const pairCount = Math.min(removedCount, addedCount);
|
|
@@ -239,25 +236,35 @@ function unifiedLayout(diff, opts) {
|
|
|
239
236
|
lineTextW: Math.max(1, textWidth - noW - 5),
|
|
240
237
|
textWidth,
|
|
241
238
|
useTrueColor: opts.trueColor !== false,
|
|
239
|
+
gutterLine: opts.gutterLine !== false,
|
|
242
240
|
lang: opts.syntaxHighlight !== false ? detectLanguage(opts.filePath) : undefined,
|
|
243
241
|
removedPalette: { rowBg: p.errorBg, emphBg: p.errorBgEmph },
|
|
244
242
|
addedPalette: { rowBg: p.successBg, emphBg: p.successBgEmph },
|
|
245
243
|
};
|
|
246
244
|
}
|
|
247
245
|
function renderUnifiedHunk(hunk, layout) {
|
|
248
|
-
const { noW, lineTextW, textWidth, useTrueColor, lang, removedPalette, addedPalette } = layout;
|
|
246
|
+
const { noW, lineTextW, textWidth, useTrueColor, gutterLine, lang, removedPalette, addedPalette } = layout;
|
|
249
247
|
const out = [];
|
|
250
248
|
const pairs = findChangePairs(hunk);
|
|
251
249
|
const renderedAsPartOfPair = new Set();
|
|
252
250
|
const bgWidth = Math.max(1, textWidth - noW - 3);
|
|
253
251
|
const gutter = (n) => `${p.dim}${n} │${p.reset} `;
|
|
252
|
+
const change = (no, sigil, bg, fg, text) => {
|
|
253
|
+
if (!gutterLine) {
|
|
254
|
+
return `${bg}${fg}${padToWidth(`${no} ${sigil} ${preserveBg(text, bg)}`, textWidth)}${p.reset}`;
|
|
255
|
+
}
|
|
256
|
+
if (useTrueColor)
|
|
257
|
+
return gutter(no) + padToWidth(`${bg}${fg}${sigil} ${preserveBg(text, bg)}`, bgWidth) + p.reset;
|
|
258
|
+
return `${gutter(no)}${fg}${sigil} ${text}${p.reset}`;
|
|
259
|
+
};
|
|
254
260
|
for (let i = 0; i < hunk.lines.length; i++) {
|
|
255
261
|
const line = hunk.lines[i];
|
|
256
262
|
const no = String(line.type === "removed" ? (line.oldNo ?? "") : (line.newNo ?? line.oldNo ?? "")).padStart(noW);
|
|
257
263
|
if (line.type === "context") {
|
|
258
264
|
const raw = truncateText(line.text, lineTextW);
|
|
259
265
|
const text = lang ? highlightLine(raw, lang) : raw;
|
|
260
|
-
|
|
266
|
+
// The flush gutter dims only the line number; the code stays normal/highlighted.
|
|
267
|
+
out.push(!gutterLine ? `${p.dim}${no}${p.reset} ${text}` : `${gutter(no)} ${p.dim}${text}${p.reset}`);
|
|
261
268
|
continue;
|
|
262
269
|
}
|
|
263
270
|
if (line.type === "removed") {
|
|
@@ -276,19 +283,9 @@ function renderUnifiedHunk(hunk, layout) {
|
|
|
276
283
|
const raw = truncateText(line.text, lineTextW);
|
|
277
284
|
removedText = lang ? highlightLine(raw, lang) : raw;
|
|
278
285
|
}
|
|
279
|
-
|
|
280
|
-
out.push(gutter(no) + padToWidth(`${p.errorBg}${p.error}- ${preserveBg(removedText, p.errorBg)}`, bgWidth) + p.reset);
|
|
281
|
-
}
|
|
282
|
-
else {
|
|
283
|
-
out.push(`${gutter(no)}${p.error}- ${removedText}${p.reset}`);
|
|
284
|
-
}
|
|
286
|
+
out.push(change(no, "-", p.errorBg, p.error, removedText));
|
|
285
287
|
if (addedText !== null && addedNo !== null) {
|
|
286
|
-
|
|
287
|
-
out.push(gutter(addedNo) + padToWidth(`${p.successBg}${p.success}+ ${preserveBg(addedText, p.successBg)}`, bgWidth) + p.reset);
|
|
288
|
-
}
|
|
289
|
-
else {
|
|
290
|
-
out.push(`${gutter(addedNo)}${p.success}+ ${addedText}${p.reset}`);
|
|
291
|
-
}
|
|
288
|
+
out.push(change(addedNo, "+", p.successBg, p.success, addedText));
|
|
292
289
|
}
|
|
293
290
|
continue;
|
|
294
291
|
}
|
|
@@ -297,12 +294,7 @@ function renderUnifiedHunk(hunk, layout) {
|
|
|
297
294
|
continue;
|
|
298
295
|
const raw = truncateText(line.text, lineTextW);
|
|
299
296
|
const text = lang ? highlightLine(raw, lang) : raw;
|
|
300
|
-
|
|
301
|
-
out.push(gutter(no) + padToWidth(`${p.successBg}${p.success}+ ${preserveBg(text, p.successBg)}`, bgWidth) + p.reset);
|
|
302
|
-
}
|
|
303
|
-
else {
|
|
304
|
-
out.push(`${gutter(no)}${p.success}+ ${text}${p.reset}`);
|
|
305
|
-
}
|
|
297
|
+
out.push(change(no, "+", p.successBg, p.success, text));
|
|
306
298
|
}
|
|
307
299
|
}
|
|
308
300
|
return out;
|
|
@@ -423,19 +415,16 @@ function buildSplitRows(hunk) {
|
|
|
423
415
|
i++;
|
|
424
416
|
continue;
|
|
425
417
|
}
|
|
426
|
-
// Collect a run of removed lines
|
|
427
418
|
const removed = [];
|
|
428
419
|
while (i < lines.length && lines[i].type === "removed") {
|
|
429
420
|
removed.push(lines[i]);
|
|
430
421
|
i++;
|
|
431
422
|
}
|
|
432
|
-
// Collect a run of added lines
|
|
433
423
|
const added = [];
|
|
434
424
|
while (i < lines.length && lines[i].type === "added") {
|
|
435
425
|
added.push(lines[i]);
|
|
436
426
|
i++;
|
|
437
427
|
}
|
|
438
|
-
// Pair them side by side
|
|
439
428
|
const maxLen = Math.max(removed.length, added.length);
|
|
440
429
|
for (let k = 0; k < maxLen; k++) {
|
|
441
430
|
rows.push({
|
|
@@ -522,7 +511,6 @@ function trimHunksToFit(hunks, maxLines) {
|
|
|
522
511
|
changeCount++;
|
|
523
512
|
}
|
|
524
513
|
}
|
|
525
|
-
// Separators between hunks
|
|
526
514
|
const separators = Math.max(0, hunks.length - 1);
|
|
527
515
|
// How many context lines can we afford?
|
|
528
516
|
const contextBudget = Math.max(0, maxLines - changeCount - separators);
|
|
@@ -50,13 +50,8 @@ export declare class HandlerRegistry {
|
|
|
50
50
|
* Returns undefined if no handler is registered.
|
|
51
51
|
*/
|
|
52
52
|
call(name: string, ...args: any[]): any;
|
|
53
|
-
/**
|
|
54
|
-
* Check if a named handler exists.
|
|
55
|
-
*/
|
|
56
53
|
has(name: string): boolean;
|
|
57
|
-
/**
|
|
58
|
-
* Names of all registered handlers. For diagnostic/introspection use.
|
|
59
|
-
*/
|
|
54
|
+
/** Names of all registered handlers — for diagnostics/introspection. */
|
|
60
55
|
list(): string[];
|
|
61
56
|
}
|
|
62
57
|
export {};
|
|
@@ -79,15 +79,10 @@ export class HandlerRegistry {
|
|
|
79
79
|
}
|
|
80
80
|
return fn(...args);
|
|
81
81
|
}
|
|
82
|
-
/**
|
|
83
|
-
* Check if a named handler exists.
|
|
84
|
-
*/
|
|
85
82
|
has(name) {
|
|
86
83
|
return this.entries.has(name);
|
|
87
84
|
}
|
|
88
|
-
/**
|
|
89
|
-
* Names of all registered handlers. For diagnostic/introspection use.
|
|
90
|
-
*/
|
|
85
|
+
/** Names of all registered handlers — for diagnostics/introspection. */
|
|
91
86
|
list() {
|
|
92
87
|
return [...this.entries.keys()];
|
|
93
88
|
}
|
|
@@ -309,11 +309,9 @@ export class LineEditor {
|
|
|
309
309
|
pushHistory(line) {
|
|
310
310
|
if (!line.trim())
|
|
311
311
|
return;
|
|
312
|
-
// Deduplicate: remove if already at top
|
|
313
312
|
if (this.history.length > 0 && this.history[0] === line)
|
|
314
313
|
return;
|
|
315
314
|
this.history.unshift(line);
|
|
316
|
-
// Cap history size
|
|
317
315
|
if (this.history.length > 100)
|
|
318
316
|
this.history.pop();
|
|
319
317
|
}
|
package/dist/utils/palette.js
CHANGED
|
@@ -14,10 +14,10 @@ const defaultPalette = {
|
|
|
14
14
|
warning: "\x1b[33m", // yellow
|
|
15
15
|
error: "\x1b[31m", // red
|
|
16
16
|
muted: "\x1b[90m", // gray
|
|
17
|
-
successBg: "\x1b[48;2;
|
|
18
|
-
errorBg: "\x1b[48;2;
|
|
19
|
-
successBgEmph: "\x1b[48;2;
|
|
20
|
-
errorBgEmph: "\x1b[48;2;
|
|
17
|
+
successBg: "\x1b[48;2;34;92;43m",
|
|
18
|
+
errorBg: "\x1b[48;2;122;41;54m",
|
|
19
|
+
successBgEmph: "\x1b[48;2;56;166;96m",
|
|
20
|
+
errorBgEmph: "\x1b[48;2;179;89;107m",
|
|
21
21
|
bold: "\x1b[1m",
|
|
22
22
|
dim: "\x1b[2m",
|
|
23
23
|
italic: "\x1b[3m",
|
|
@@ -47,6 +47,8 @@ export declare class TerminalBuffer {
|
|
|
47
47
|
readScreen(opts?: {
|
|
48
48
|
includeScrollback?: boolean;
|
|
49
49
|
}): ScreenSnapshot;
|
|
50
|
+
/** Read the screen and wrap it as a `<terminal_buffer>` context block. */
|
|
51
|
+
formatScreen(maxLines?: number, baseContext?: string): string;
|
|
50
52
|
/**
|
|
51
53
|
* Get terminal screen as lines, padded/trimmed to exactly `rows` lines.
|
|
52
54
|
* Clean text only (ANSI stripped). Reads from the active buffer's
|
|
@@ -111,6 +111,10 @@ export class TerminalBuffer {
|
|
|
111
111
|
cursorY: buf.cursorY,
|
|
112
112
|
};
|
|
113
113
|
}
|
|
114
|
+
/** Read the screen and wrap it as a `<terminal_buffer>` context block. */
|
|
115
|
+
formatScreen(maxLines, baseContext) {
|
|
116
|
+
return formatScreenContext(this.readScreen(), maxLines, baseContext);
|
|
117
|
+
}
|
|
114
118
|
/**
|
|
115
119
|
* Get terminal screen as lines, padded/trimmed to exactly `rows` lines.
|
|
116
120
|
* Clean text only (ANSI stripped). Reads from the active buffer's
|