fix: tui local shell consent UX (#1463)
- add local shell runner + denial notice + tests - docs: describe ! local shell usage - lint: drop unused Slack upload contentType - cleanup: remove stray Swabble pins Thanks @vignesh07. Co-authored-by: Vignesh Natarajan <vigneshnatarajan92@gmail.com>
This commit is contained in:
@@ -5,6 +5,7 @@ Docs: https://docs.clawd.bot
|
|||||||
## 2026.1.22 (unreleased)
|
## 2026.1.22 (unreleased)
|
||||||
|
|
||||||
### Changes
|
### Changes
|
||||||
|
- TUI: run local shell commands with `!` after per-session consent, and warn when local exec stays disabled. (#1463) Thanks @vignesh07.
|
||||||
- Highlight: Lobster optional plugin tool for typed workflows + approval gates. https://docs.clawd.bot/tools/lobster
|
- Highlight: Lobster optional plugin tool for typed workflows + approval gates. https://docs.clawd.bot/tools/lobster
|
||||||
- Agents: add identity avatar config support and Control UI avatar rendering. (#1329, #1424) Thanks @dlauer.
|
- Agents: add identity avatar config support and Control UI avatar rendering. (#1329, #1424) Thanks @dlauer.
|
||||||
- Memory: prevent CLI hangs by deferring vector probes, adding sqlite-vec/embedding timeouts, and showing sync progress early.
|
- Memory: prevent CLI hangs by deferring vector probes, adding sqlite-vec/embedding timeouts, and showing sync progress early.
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
{
|
{
|
||||||
"originHash" : "10946d10a137b295c88bae6eb5b1d11f637a00bde46464b19cbf059f77d05e6f",
|
"originHash" : "c0677e232394b5f6b0191b6dbb5bae553d55264f65ae725cd03a8ffdfda9cdd3",
|
||||||
"pins" : [
|
"pins" : [
|
||||||
{
|
{
|
||||||
"identity" : "commander",
|
"identity" : "commander",
|
||||||
@@ -10,24 +10,6 @@
|
|||||||
"version" : "0.2.1"
|
"version" : "0.2.1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"identity" : "elevenlabskit",
|
|
||||||
"kind" : "remoteSourceControl",
|
|
||||||
"location" : "https://github.com/steipete/ElevenLabsKit",
|
|
||||||
"state" : {
|
|
||||||
"revision" : "7e3c948d8340abe3977014f3de020edf221e9269",
|
|
||||||
"version" : "0.1.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"identity" : "swift-concurrency-extras",
|
|
||||||
"kind" : "remoteSourceControl",
|
|
||||||
"location" : "https://github.com/pointfreeco/swift-concurrency-extras",
|
|
||||||
"state" : {
|
|
||||||
"revision" : "5a3825302b1a0d744183200915a47b508c828e6f",
|
|
||||||
"version" : "1.3.2"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"identity" : "swift-syntax",
|
"identity" : "swift-syntax",
|
||||||
"kind" : "remoteSourceControl",
|
"kind" : "remoteSourceControl",
|
||||||
@@ -45,24 +27,6 @@
|
|||||||
"revision" : "399f76dcd91e4c688ca2301fa24a8cc6d9927211",
|
"revision" : "399f76dcd91e4c688ca2301fa24a8cc6d9927211",
|
||||||
"version" : "0.99.0"
|
"version" : "0.99.0"
|
||||||
}
|
}
|
||||||
},
|
|
||||||
{
|
|
||||||
"identity" : "swiftui-math",
|
|
||||||
"kind" : "remoteSourceControl",
|
|
||||||
"location" : "https://github.com/gonzalezreal/swiftui-math",
|
|
||||||
"state" : {
|
|
||||||
"revision" : "0b5c2cfaaec8d6193db206f675048eeb5ce95f71",
|
|
||||||
"version" : "0.1.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"identity" : "textual",
|
|
||||||
"kind" : "remoteSourceControl",
|
|
||||||
"location" : "https://github.com/gonzalezreal/textual",
|
|
||||||
"state" : {
|
|
||||||
"revision" : "a03c1e103d88de4ea0dd8320ea1611ec0d4b29b3",
|
|
||||||
"version" : "0.2.0"
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"version" : 3
|
"version" : 3
|
||||||
|
|||||||
@@ -88,6 +88,12 @@ Session lifecycle:
|
|||||||
- `/settings`
|
- `/settings`
|
||||||
- `/exit`
|
- `/exit`
|
||||||
|
|
||||||
|
## Local shell commands
|
||||||
|
- Prefix a line with `!` to run a local shell command on the TUI host.
|
||||||
|
- The TUI prompts once per session to allow local execution; declining keeps `!` disabled for the session.
|
||||||
|
- Commands run in a fresh, non-interactive shell in the TUI working directory (no persistent `cd`/env).
|
||||||
|
- A lone `!` is sent as a normal message; leading spaces do not trigger local exec.
|
||||||
|
|
||||||
## Tool output
|
## Tool output
|
||||||
- Tool calls show as cards with args + results.
|
- Tool calls show as cards with args + results.
|
||||||
- Ctrl+O toggles between collapsed/expanded views.
|
- Ctrl+O toggles between collapsed/expanded views.
|
||||||
|
|||||||
@@ -88,7 +88,7 @@ async function uploadSlackFile(params: {
|
|||||||
threadTs?: string;
|
threadTs?: string;
|
||||||
maxBytes?: number;
|
maxBytes?: number;
|
||||||
}): Promise<string> {
|
}): Promise<string> {
|
||||||
const { buffer, contentType, fileName } = await loadWebMedia(params.mediaUrl, params.maxBytes);
|
const { buffer, fileName } = await loadWebMedia(params.mediaUrl, params.maxBytes);
|
||||||
const basePayload = {
|
const basePayload = {
|
||||||
channel_id: params.channelId,
|
channel_id: params.channelId,
|
||||||
file: buffer,
|
file: buffer,
|
||||||
|
|||||||
@@ -58,6 +58,24 @@ describe("createEditorSubmitHandler", () => {
|
|||||||
expect(editor.addToHistory).not.toHaveBeenCalled();
|
expect(editor.addToHistory).not.toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("does not add whitespace-only submissions to history", () => {
|
||||||
|
const editor = {
|
||||||
|
setText: vi.fn(),
|
||||||
|
addToHistory: vi.fn(),
|
||||||
|
};
|
||||||
|
|
||||||
|
const handler = createEditorSubmitHandler({
|
||||||
|
editor,
|
||||||
|
handleCommand: vi.fn(),
|
||||||
|
sendMessage: vi.fn(),
|
||||||
|
handleBangLine: vi.fn(),
|
||||||
|
});
|
||||||
|
|
||||||
|
handler(" ");
|
||||||
|
|
||||||
|
expect(editor.addToHistory).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
it("routes slash commands to handleCommand", () => {
|
it("routes slash commands to handleCommand", () => {
|
||||||
const editor = {
|
const editor = {
|
||||||
setText: vi.fn(),
|
setText: vi.fn(),
|
||||||
|
|||||||
54
src/tui/tui-local-shell.test.ts
Normal file
54
src/tui/tui-local-shell.test.ts
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
import { describe, expect, it, vi } from "vitest";
|
||||||
|
|
||||||
|
import { createLocalShellRunner } from "./tui-local-shell.js";
|
||||||
|
|
||||||
|
const createSelector = () => {
|
||||||
|
const selector = {
|
||||||
|
onSelect: undefined as ((item: { value: string; label: string }) => void) | undefined,
|
||||||
|
onCancel: undefined as (() => void) | undefined,
|
||||||
|
render: () => ["selector"],
|
||||||
|
invalidate: () => {},
|
||||||
|
};
|
||||||
|
return selector;
|
||||||
|
};
|
||||||
|
|
||||||
|
describe("createLocalShellRunner", () => {
|
||||||
|
it("logs denial on subsequent ! attempts without re-prompting", async () => {
|
||||||
|
const messages: string[] = [];
|
||||||
|
const chatLog = {
|
||||||
|
addSystem: (line: string) => {
|
||||||
|
messages.push(line);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
const tui = { requestRender: vi.fn() };
|
||||||
|
const openOverlay = vi.fn();
|
||||||
|
const closeOverlay = vi.fn();
|
||||||
|
let lastSelector: ReturnType<typeof createSelector> | null = null;
|
||||||
|
const createSelectorSpy = vi.fn(() => {
|
||||||
|
lastSelector = createSelector();
|
||||||
|
return lastSelector;
|
||||||
|
});
|
||||||
|
const spawnCommand = vi.fn();
|
||||||
|
|
||||||
|
const { runLocalShellLine } = createLocalShellRunner({
|
||||||
|
chatLog,
|
||||||
|
tui,
|
||||||
|
openOverlay,
|
||||||
|
closeOverlay,
|
||||||
|
createSelector: createSelectorSpy,
|
||||||
|
spawnCommand,
|
||||||
|
});
|
||||||
|
|
||||||
|
const firstRun = runLocalShellLine("!ls");
|
||||||
|
expect(openOverlay).toHaveBeenCalledTimes(1);
|
||||||
|
lastSelector?.onSelect?.({ value: "no", label: "No" });
|
||||||
|
await firstRun;
|
||||||
|
|
||||||
|
await runLocalShellLine("!pwd");
|
||||||
|
|
||||||
|
expect(messages).toContain("local shell: not enabled");
|
||||||
|
expect(messages).toContain("local shell: not enabled for this session");
|
||||||
|
expect(createSelectorSpy).toHaveBeenCalledTimes(1);
|
||||||
|
expect(spawnCommand).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
||||||
137
src/tui/tui-local-shell.ts
Normal file
137
src/tui/tui-local-shell.ts
Normal file
@@ -0,0 +1,137 @@
|
|||||||
|
import type { Component, SelectItem } from "@mariozechner/pi-tui";
|
||||||
|
import { spawn } from "node:child_process";
|
||||||
|
import { createSearchableSelectList } from "./components/selectors.js";
|
||||||
|
|
||||||
|
type LocalShellDeps = {
|
||||||
|
chatLog: {
|
||||||
|
addSystem: (line: string) => void;
|
||||||
|
};
|
||||||
|
tui: {
|
||||||
|
requestRender: () => void;
|
||||||
|
};
|
||||||
|
openOverlay: (component: Component) => void;
|
||||||
|
closeOverlay: () => void;
|
||||||
|
createSelector?: (
|
||||||
|
items: SelectItem[],
|
||||||
|
maxVisible: number,
|
||||||
|
) => Component & {
|
||||||
|
onSelect?: (item: SelectItem) => void;
|
||||||
|
onCancel?: () => void;
|
||||||
|
};
|
||||||
|
spawnCommand?: typeof spawn;
|
||||||
|
getCwd?: () => string;
|
||||||
|
env?: NodeJS.ProcessEnv;
|
||||||
|
maxOutputChars?: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function createLocalShellRunner(deps: LocalShellDeps) {
|
||||||
|
let localExecAsked = false;
|
||||||
|
let localExecAllowed = false;
|
||||||
|
const createSelector = deps.createSelector ?? createSearchableSelectList;
|
||||||
|
const spawnCommand = deps.spawnCommand ?? spawn;
|
||||||
|
const getCwd = deps.getCwd ?? (() => process.cwd());
|
||||||
|
const env = deps.env ?? process.env;
|
||||||
|
const maxChars = deps.maxOutputChars ?? 40_000;
|
||||||
|
|
||||||
|
const ensureLocalExecAllowed = async (): Promise<boolean> => {
|
||||||
|
if (localExecAllowed) return true;
|
||||||
|
if (localExecAsked) return false;
|
||||||
|
localExecAsked = true;
|
||||||
|
|
||||||
|
return await new Promise<boolean>((resolve) => {
|
||||||
|
deps.chatLog.addSystem("Allow local shell commands for this session?");
|
||||||
|
deps.chatLog.addSystem(
|
||||||
|
"This runs commands on YOUR machine (not the gateway) and may delete files or reveal secrets.",
|
||||||
|
);
|
||||||
|
deps.chatLog.addSystem("Select Yes/No (arrows + Enter), Esc to cancel.");
|
||||||
|
const selector = createSelector(
|
||||||
|
[
|
||||||
|
{ value: "no", label: "No" },
|
||||||
|
{ value: "yes", label: "Yes" },
|
||||||
|
],
|
||||||
|
2,
|
||||||
|
);
|
||||||
|
selector.onSelect = (item) => {
|
||||||
|
deps.closeOverlay();
|
||||||
|
if (item.value === "yes") {
|
||||||
|
localExecAllowed = true;
|
||||||
|
deps.chatLog.addSystem("local shell: enabled for this session");
|
||||||
|
resolve(true);
|
||||||
|
} else {
|
||||||
|
deps.chatLog.addSystem("local shell: not enabled");
|
||||||
|
resolve(false);
|
||||||
|
}
|
||||||
|
deps.tui.requestRender();
|
||||||
|
};
|
||||||
|
selector.onCancel = () => {
|
||||||
|
deps.closeOverlay();
|
||||||
|
deps.chatLog.addSystem("local shell: cancelled");
|
||||||
|
deps.tui.requestRender();
|
||||||
|
resolve(false);
|
||||||
|
};
|
||||||
|
deps.openOverlay(selector);
|
||||||
|
deps.tui.requestRender();
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const runLocalShellLine = async (line: string) => {
|
||||||
|
const cmd = line.slice(1);
|
||||||
|
// NOTE: A lone '!' is handled by the submit handler as a normal message.
|
||||||
|
// Keep this guard anyway in case this is called directly.
|
||||||
|
if (cmd === "") return;
|
||||||
|
|
||||||
|
if (localExecAsked && !localExecAllowed) {
|
||||||
|
deps.chatLog.addSystem("local shell: not enabled for this session");
|
||||||
|
deps.tui.requestRender();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const allowed = await ensureLocalExecAllowed();
|
||||||
|
if (!allowed) return;
|
||||||
|
|
||||||
|
deps.chatLog.addSystem(`[local] $ ${cmd}`);
|
||||||
|
deps.tui.requestRender();
|
||||||
|
|
||||||
|
await new Promise<void>((resolve) => {
|
||||||
|
const child = spawnCommand(cmd, {
|
||||||
|
shell: true,
|
||||||
|
cwd: getCwd(),
|
||||||
|
env,
|
||||||
|
});
|
||||||
|
|
||||||
|
let stdout = "";
|
||||||
|
let stderr = "";
|
||||||
|
child.stdout.on("data", (buf) => {
|
||||||
|
stdout += buf.toString("utf8");
|
||||||
|
});
|
||||||
|
child.stderr.on("data", (buf) => {
|
||||||
|
stderr += buf.toString("utf8");
|
||||||
|
});
|
||||||
|
|
||||||
|
child.on("close", (code, signal) => {
|
||||||
|
const combined = (stdout + (stderr ? (stdout ? "\n" : "") + stderr : ""))
|
||||||
|
.slice(0, maxChars)
|
||||||
|
.trimEnd();
|
||||||
|
|
||||||
|
if (combined) {
|
||||||
|
for (const line of combined.split("\n")) {
|
||||||
|
deps.chatLog.addSystem(`[local] ${line}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
deps.chatLog.addSystem(
|
||||||
|
`[local] exit ${code ?? "?"}${signal ? ` (signal ${String(signal)})` : ""}`,
|
||||||
|
);
|
||||||
|
deps.tui.requestRender();
|
||||||
|
resolve();
|
||||||
|
});
|
||||||
|
|
||||||
|
child.on("error", (err) => {
|
||||||
|
deps.chatLog.addSystem(`[local] error: ${String(err)}`);
|
||||||
|
deps.tui.requestRender();
|
||||||
|
resolve();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
return { runLocalShellLine };
|
||||||
|
}
|
||||||
@@ -50,6 +50,29 @@ describe("createEditorSubmitHandler", () => {
|
|||||||
expect(sendMessage).toHaveBeenCalledWith("!");
|
expect(sendMessage).toHaveBeenCalledWith("!");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("does not treat leading whitespace before ! as a bang command", () => {
|
||||||
|
const editor = {
|
||||||
|
setText: vi.fn(),
|
||||||
|
addToHistory: vi.fn(),
|
||||||
|
};
|
||||||
|
const handleCommand = vi.fn();
|
||||||
|
const sendMessage = vi.fn();
|
||||||
|
const handleBangLine = vi.fn();
|
||||||
|
|
||||||
|
const onSubmit = createEditorSubmitHandler({
|
||||||
|
editor,
|
||||||
|
handleCommand,
|
||||||
|
sendMessage,
|
||||||
|
handleBangLine,
|
||||||
|
});
|
||||||
|
|
||||||
|
onSubmit(" !ls");
|
||||||
|
|
||||||
|
expect(handleBangLine).not.toHaveBeenCalled();
|
||||||
|
expect(sendMessage).toHaveBeenCalledWith("!ls");
|
||||||
|
expect(editor.addToHistory).toHaveBeenCalledWith("!ls");
|
||||||
|
});
|
||||||
|
|
||||||
it("trims normal messages before sending and adding to history", () => {
|
it("trims normal messages before sending and adding to history", () => {
|
||||||
const editor = {
|
const editor = {
|
||||||
setText: vi.fn(),
|
setText: vi.fn(),
|
||||||
|
|||||||
108
src/tui/tui.ts
108
src/tui/tui.ts
@@ -6,7 +6,6 @@ import {
|
|||||||
Text,
|
Text,
|
||||||
TUI,
|
TUI,
|
||||||
} from "@mariozechner/pi-tui";
|
} from "@mariozechner/pi-tui";
|
||||||
import { spawn } from "node:child_process";
|
|
||||||
import { resolveDefaultAgentId } from "../agents/agent-scope.js";
|
import { resolveDefaultAgentId } from "../agents/agent-scope.js";
|
||||||
import { loadConfig } from "../config/config.js";
|
import { loadConfig } from "../config/config.js";
|
||||||
import {
|
import {
|
||||||
@@ -22,8 +21,8 @@ import { GatewayChatClient } from "./gateway-chat.js";
|
|||||||
import { editorTheme, theme } from "./theme/theme.js";
|
import { editorTheme, theme } from "./theme/theme.js";
|
||||||
import { createCommandHandlers } from "./tui-command-handlers.js";
|
import { createCommandHandlers } from "./tui-command-handlers.js";
|
||||||
import { createEventHandlers } from "./tui-event-handlers.js";
|
import { createEventHandlers } from "./tui-event-handlers.js";
|
||||||
import { createSearchableSelectList } from "./components/selectors.js";
|
|
||||||
import { formatTokens } from "./tui-formatters.js";
|
import { formatTokens } from "./tui-formatters.js";
|
||||||
|
import { createLocalShellRunner } from "./tui-local-shell.js";
|
||||||
import { buildWaitingStatusMessage, defaultWaitingPhrases } from "./tui-waiting.js";
|
import { buildWaitingStatusMessage, defaultWaitingPhrases } from "./tui-waiting.js";
|
||||||
import { createOverlayHandlers } from "./tui-overlays.js";
|
import { createOverlayHandlers } from "./tui-overlays.js";
|
||||||
import { createSessionActions } from "./tui-session-actions.js";
|
import { createSessionActions } from "./tui-session-actions.js";
|
||||||
@@ -94,11 +93,6 @@ export async function runTui(opts: TuiOptions) {
|
|||||||
let toolsExpanded = false;
|
let toolsExpanded = false;
|
||||||
let showThinking = false;
|
let showThinking = false;
|
||||||
|
|
||||||
// Local "bash mode" (lines starting with '!') state.
|
|
||||||
// Permission is asked once per TUI session.
|
|
||||||
let localExecAsked = false;
|
|
||||||
let localExecAllowed = false;
|
|
||||||
|
|
||||||
const deliverDefault = opts.deliver ?? false;
|
const deliverDefault = opts.deliver ?? false;
|
||||||
const autoMessage = opts.message?.trim();
|
const autoMessage = opts.message?.trim();
|
||||||
let autoMessageSent = false;
|
let autoMessageSent = false;
|
||||||
@@ -518,102 +512,12 @@ export async function runTui(opts: TuiOptions) {
|
|||||||
formatSessionKey,
|
formatSessionKey,
|
||||||
});
|
});
|
||||||
|
|
||||||
const ensureLocalExecAllowed = async (): Promise<boolean> => {
|
const { runLocalShellLine } = createLocalShellRunner({
|
||||||
if (localExecAllowed) return true;
|
chatLog,
|
||||||
if (localExecAsked) return false;
|
tui,
|
||||||
localExecAsked = true;
|
openOverlay,
|
||||||
|
closeOverlay,
|
||||||
return await new Promise<boolean>((resolve) => {
|
|
||||||
chatLog.addSystem("Allow local shell commands for this session?");
|
|
||||||
chatLog.addSystem(
|
|
||||||
"This runs commands on YOUR machine (not the gateway) and may delete files or reveal secrets.",
|
|
||||||
);
|
|
||||||
chatLog.addSystem("Select Yes/No (arrows + Enter), Esc to cancel.");
|
|
||||||
const selector = createSearchableSelectList(
|
|
||||||
[
|
|
||||||
{ value: "no", label: "No" },
|
|
||||||
{ value: "yes", label: "Yes" },
|
|
||||||
],
|
|
||||||
2,
|
|
||||||
);
|
|
||||||
selector.onSelect = (item) => {
|
|
||||||
closeOverlay();
|
|
||||||
if (item.value === "yes") {
|
|
||||||
localExecAllowed = true;
|
|
||||||
chatLog.addSystem("local shell: enabled for this session");
|
|
||||||
resolve(true);
|
|
||||||
} else {
|
|
||||||
chatLog.addSystem("local shell: not enabled");
|
|
||||||
resolve(false);
|
|
||||||
}
|
|
||||||
tui.requestRender();
|
|
||||||
};
|
|
||||||
selector.onCancel = () => {
|
|
||||||
closeOverlay();
|
|
||||||
chatLog.addSystem("local shell: cancelled");
|
|
||||||
tui.requestRender();
|
|
||||||
resolve(false);
|
|
||||||
};
|
|
||||||
openOverlay(selector);
|
|
||||||
tui.requestRender();
|
|
||||||
});
|
});
|
||||||
};
|
|
||||||
|
|
||||||
const runLocalShellLine = async (line: string) => {
|
|
||||||
// line starts with '!'
|
|
||||||
const cmd = line.slice(1);
|
|
||||||
// NOTE: A lone '!' is handled by the submit handler as a normal message.
|
|
||||||
// Keep this guard anyway in case this is called directly.
|
|
||||||
if (cmd === "") return;
|
|
||||||
|
|
||||||
const allowed = await ensureLocalExecAllowed();
|
|
||||||
if (!allowed) return;
|
|
||||||
|
|
||||||
chatLog.addSystem(`[local] $ ${cmd}`);
|
|
||||||
tui.requestRender();
|
|
||||||
|
|
||||||
await new Promise<void>((resolve) => {
|
|
||||||
const child = spawn(cmd, {
|
|
||||||
shell: true,
|
|
||||||
cwd: process.cwd(),
|
|
||||||
env: process.env,
|
|
||||||
});
|
|
||||||
|
|
||||||
let stdout = "";
|
|
||||||
let stderr = "";
|
|
||||||
child.stdout.on("data", (buf) => {
|
|
||||||
stdout += buf.toString("utf8");
|
|
||||||
});
|
|
||||||
child.stderr.on("data", (buf) => {
|
|
||||||
stderr += buf.toString("utf8");
|
|
||||||
});
|
|
||||||
|
|
||||||
child.on("close", (code, signal) => {
|
|
||||||
const maxChars = 40_000;
|
|
||||||
const combined = (stdout + (stderr ? (stdout ? "\n" : "") + stderr : ""))
|
|
||||||
.slice(0, maxChars)
|
|
||||||
.trimEnd();
|
|
||||||
|
|
||||||
if (combined) {
|
|
||||||
for (const line of combined.split("\n")) {
|
|
||||||
chatLog.addSystem(`[local] ${line}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
chatLog.addSystem(
|
|
||||||
`[local] exit ${code ?? "?"}${signal ? ` (signal ${String(signal)})` : ""}`,
|
|
||||||
);
|
|
||||||
tui.requestRender();
|
|
||||||
resolve();
|
|
||||||
});
|
|
||||||
|
|
||||||
child.on("error", (err) => {
|
|
||||||
chatLog.addSystem(`[local] error: ${String(err)}`);
|
|
||||||
tui.requestRender();
|
|
||||||
resolve();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
updateAutocompleteProvider();
|
updateAutocompleteProvider();
|
||||||
editor.onSubmit = createEditorSubmitHandler({
|
editor.onSubmit = createEditorSubmitHandler({
|
||||||
editor,
|
editor,
|
||||||
|
|||||||
Reference in New Issue
Block a user