Archived
Add shell execution from chat
This commit is contained in:
@@ -92,6 +92,7 @@ export const api = {
|
|||||||
commands: (sessionId: string) => request<SlashCommand[]>(`/api/sessions/${sessionId}/commands`),
|
commands: (sessionId: string) => request<SlashCommand[]>(`/api/sessions/${sessionId}/commands`),
|
||||||
files: (cwd: string, query: string, kind?: FileSuggestion["kind"]) => request<FileSuggestion[]>(`/api/files?cwd=${encodeURIComponent(cwd)}&q=${encodeURIComponent(query)}${kind ? `&kind=${encodeURIComponent(kind)}` : ""}`),
|
files: (cwd: string, query: string, kind?: FileSuggestion["kind"]) => request<FileSuggestion[]>(`/api/files?cwd=${encodeURIComponent(cwd)}&q=${encodeURIComponent(query)}${kind ? `&kind=${encodeURIComponent(kind)}` : ""}`),
|
||||||
prompt: (sessionId: string, text: string) => request<{ accepted: true }>(`/api/sessions/${sessionId}/prompt`, { method: "POST", body: JSON.stringify({ text }) }),
|
prompt: (sessionId: string, text: string) => request<{ accepted: true }>(`/api/sessions/${sessionId}/prompt`, { method: "POST", body: JSON.stringify({ text }) }),
|
||||||
|
shell: (sessionId: string, text: string) => request<{ accepted: true }>(`/api/sessions/${sessionId}/shell`, { method: "POST", body: JSON.stringify({ text }) }),
|
||||||
runCommand: (sessionId: string, text: string) => request<CommandResult>(`/api/sessions/${sessionId}/commands/run`, { method: "POST", body: JSON.stringify({ text }) }),
|
runCommand: (sessionId: string, text: string) => request<CommandResult>(`/api/sessions/${sessionId}/commands/run`, { method: "POST", body: JSON.stringify({ text }) }),
|
||||||
respondToCommand: (sessionId: string, requestId: string, value: string) => request<CommandResult>(`/api/sessions/${sessionId}/commands/respond`, { method: "POST", body: JSON.stringify({ requestId, value }) }),
|
respondToCommand: (sessionId: string, requestId: string, value: string) => request<CommandResult>(`/api/sessions/${sessionId}/commands/respond`, { method: "POST", body: JSON.stringify({ requestId, value }) }),
|
||||||
stop: (sessionId: string) => request<{ stopped: true }>(`/api/sessions/${sessionId}/stop`, { method: "POST" }),
|
stop: (sessionId: string) => request<{ stopped: true }>(`/api/sessions/${sessionId}/stop`, { method: "POST" }),
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ export function appendText(messages: ChatLine[], role: ChatLine["role"], text: s
|
|||||||
}
|
}
|
||||||
|
|
||||||
function normalizeMessage(message: any): ChatLine[] {
|
function normalizeMessage(message: any): ChatLine[] {
|
||||||
|
if (message?.role === "bashExecution") return [normalizeBashExecution(message)];
|
||||||
const role = normalizeRole(message?.role);
|
const role = normalizeRole(message?.role);
|
||||||
const parts = normalizeContent(message?.content, message);
|
const parts = normalizeContent(message?.content, message);
|
||||||
if (role === "tool") return [{ role, parts }];
|
if (role === "tool") return [{ role, parts }];
|
||||||
@@ -29,6 +30,17 @@ function normalizeMessage(message: any): ChatLine[] {
|
|||||||
return visible.length ? [{ role, parts: visible }] : [];
|
return visible.length ? [{ role, parts: visible }] : [];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function normalizeBashExecution(message: any): ChatLine {
|
||||||
|
const lines = [`$ ${message.command ?? ""}`];
|
||||||
|
if (message.output) lines.push("", String(message.output));
|
||||||
|
if (message.exitCode != null) lines.push("", `exit ${message.exitCode}`);
|
||||||
|
if (message.cancelled) lines.push("", "cancelled");
|
||||||
|
if (message.truncated) lines.push("", "output truncated");
|
||||||
|
if (message.fullOutputPath) lines.push("", `full output: ${message.fullOutputPath}`);
|
||||||
|
if (message.excludeFromContext) lines.push("", "excluded from context");
|
||||||
|
return { role: "bash", parts: [{ type: "text", text: lines.join("\n") }] };
|
||||||
|
}
|
||||||
|
|
||||||
function normalizeRole(role: unknown): ChatLine["role"] {
|
function normalizeRole(role: unknown): ChatLine["role"] {
|
||||||
if (role === "assistant") return "assistant";
|
if (role === "assistant") return "assistant";
|
||||||
if (role === "user") return "user";
|
if (role === "user") return "user";
|
||||||
|
|||||||
@@ -95,7 +95,7 @@ export class ChatView extends LitElement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private isReadablePart(message: ChatLine, part: ChatPart): boolean {
|
private isReadablePart(message: ChatLine, part: ChatPart): boolean {
|
||||||
return part.type === "text" && (message.role === "user" || message.role === "assistant" || message.role === "system");
|
return part.type === "text" && (message.role === "user" || message.role === "assistant" || message.role === "system" || message.role === "bash");
|
||||||
}
|
}
|
||||||
|
|
||||||
private groupSummary(messages: ChatLine[]): string {
|
private groupSummary(messages: ChatLine[]): string {
|
||||||
|
|||||||
@@ -26,9 +26,14 @@ export class PromptEditor extends LitElement {
|
|||||||
this.selectedIndex = 0;
|
this.selectedIndex = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected updated(changed: PropertyValues) {
|
||||||
|
if (changed.has("draft") || changed.has("sessionId")) this.resizeTextarea();
|
||||||
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
|
const shellMode = this.isShellMode();
|
||||||
return html`
|
return html`
|
||||||
<footer>
|
<footer class=${shellMode ? "shell-mode" : ""}>
|
||||||
<div class="editor-wrap">
|
<div class="editor-wrap">
|
||||||
<textarea
|
<textarea
|
||||||
.value=${this.draft}
|
.value=${this.draft}
|
||||||
@@ -37,6 +42,7 @@ export class PromptEditor extends LitElement {
|
|||||||
@keydown=${(event: KeyboardEvent) => this.handleKeyDown(event)}
|
@keydown=${(event: KeyboardEvent) => this.handleKeyDown(event)}
|
||||||
placeholder="Message pi... Use / for commands, @ for files"
|
placeholder="Message pi... Use / for commands, @ for files"
|
||||||
></textarea>
|
></textarea>
|
||||||
|
${shellMode ? html`<div class="mode-hint">Shell command${this.isShellExcludedFromContext() ? " · excluded from context" : ""}</div>` : null}
|
||||||
<autocomplete-menu .items=${this.completions} .selectedIndex=${this.selectedIndex} .onPick=${(item: CompletionItem) => this.pick(item)}></autocomplete-menu>
|
<autocomplete-menu .items=${this.completions} .selectedIndex=${this.selectedIndex} .onPick=${(item: CompletionItem) => this.pick(item)}></autocomplete-menu>
|
||||||
</div>
|
</div>
|
||||||
<button ?disabled=${this.disabled} @click=${this.send}>Send</button>
|
<button ?disabled=${this.disabled} @click=${this.send}>Send</button>
|
||||||
@@ -49,6 +55,21 @@ export class PromptEditor extends LitElement {
|
|||||||
this.textarea?.focus();
|
this.textarea?.focus();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private resizeTextarea() {
|
||||||
|
const textarea = this.textarea;
|
||||||
|
if (!textarea) return;
|
||||||
|
textarea.style.height = "auto";
|
||||||
|
textarea.style.height = `${textarea.scrollHeight}px`;
|
||||||
|
}
|
||||||
|
|
||||||
|
private isShellMode(): boolean {
|
||||||
|
return this.draft.trimStart().startsWith("!");
|
||||||
|
}
|
||||||
|
|
||||||
|
private isShellExcludedFromContext(): boolean {
|
||||||
|
return this.draft.trimStart().startsWith("!!");
|
||||||
|
}
|
||||||
|
|
||||||
private updateDraft(value: string) {
|
private updateDraft(value: string) {
|
||||||
this.draft = value;
|
this.draft = value;
|
||||||
if (this.sessionId) saveDraft(this.sessionId, this.draft);
|
if (this.sessionId) saveDraft(this.sessionId, this.draft);
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ export type ChatPart =
|
|||||||
| { type: "empty" };
|
| { type: "empty" };
|
||||||
|
|
||||||
export interface ChatLine {
|
export interface ChatLine {
|
||||||
role: "user" | "assistant" | "tool" | "system";
|
role: "user" | "assistant" | "tool" | "system" | "bash";
|
||||||
parts: ChatPart[];
|
parts: ChatPart[];
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -55,6 +55,7 @@ export const chatStyles = css`
|
|||||||
.msg.user { border-color: #2f81f7; background: #0d2847; }
|
.msg.user { border-color: #2f81f7; background: #0d2847; }
|
||||||
.msg.tool { border-color: #6e5200; background: #1f1a10; color: #d29922; }
|
.msg.tool { border-color: #6e5200; background: #1f1a10; color: #d29922; }
|
||||||
.msg.system { color: #ff7b72; }
|
.msg.system { color: #ff7b72; }
|
||||||
|
.msg.bash { border-color: #3fb950; background: #0f1b12; }
|
||||||
.msg.event-group { padding: 0; border-color: #30363d; background: #0d1117; color: #8b949e; }
|
.msg.event-group { padding: 0; border-color: #30363d; background: #0d1117; color: #8b949e; }
|
||||||
.msg.event-group > summary { display: flex; align-items: center; gap: 8px; padding: 8px 12px; color: #8b949e; }
|
.msg.event-group > summary { display: flex; align-items: center; gap: 8px; padding: 8px 12px; color: #8b949e; }
|
||||||
.msg.event-group > summary .label { margin: 0; }
|
.msg.event-group > summary .label { margin: 0; }
|
||||||
@@ -62,6 +63,7 @@ export const chatStyles = css`
|
|||||||
.group-msg { padding: 10px 0; border-top: 1px solid #21262d; color: #e6edf3; }
|
.group-msg { padding: 10px 0; border-top: 1px solid #21262d; color: #e6edf3; }
|
||||||
.group-msg.tool { color: #d29922; }
|
.group-msg.tool { color: #d29922; }
|
||||||
.group-msg.system { color: #ff7b72; }
|
.group-msg.system { color: #ff7b72; }
|
||||||
|
.group-msg.bash { color: #3fb950; }
|
||||||
.label { display: block; margin-bottom: 8px; color: #8b949e; font-size: 12px; text-transform: uppercase; }
|
.label { display: block; margin-bottom: 8px; color: #8b949e; font-size: 12px; text-transform: uppercase; }
|
||||||
formatted-text.part { display: block; }
|
formatted-text.part { display: block; }
|
||||||
.part + .part { margin-top: 10px; }
|
.part + .part { margin-top: 10px; }
|
||||||
@@ -134,8 +136,11 @@ export const commandPickerStyles = css`
|
|||||||
export const promptEditorStyles = css`
|
export const promptEditorStyles = css`
|
||||||
:host { display: block; color: #e6edf3; font: 14px system-ui, sans-serif; }
|
:host { display: block; color: #e6edf3; font: 14px system-ui, sans-serif; }
|
||||||
footer { display: grid; grid-template-columns: 1fr auto auto; gap: 8px; padding: 12px; border-top: 1px solid #30363d; }
|
footer { display: grid; grid-template-columns: 1fr auto auto; gap: 8px; padding: 12px; border-top: 1px solid #30363d; }
|
||||||
|
footer.shell-mode { border-top-color: #3fb950; background: #0f1b12; }
|
||||||
.editor-wrap { position: relative; min-width: 0; }
|
.editor-wrap { position: relative; min-width: 0; }
|
||||||
textarea { box-sizing: border-box; width: 100%; min-height: 54px; resize: vertical; border-radius: 8px; border: 1px solid #30363d; background: #0d1117; color: #e6edf3; padding: 8px; }
|
textarea { box-sizing: border-box; width: 100%; min-height: 54px; max-height: 220px; resize: none; overflow-y: auto; border-radius: 8px; border: 1px solid #30363d; background: #0d1117; color: #e6edf3; padding: 8px; }
|
||||||
|
.shell-mode textarea { border-color: #3fb950; box-shadow: 0 0 0 1px #3fb95055; }
|
||||||
|
.mode-hint { position: absolute; right: 8px; bottom: 8px; max-width: calc(100% - 16px); border: 1px solid #238636; border-radius: 999px; background: #0f2a16; color: #3fb950; padding: 2px 8px; font-size: 12px; pointer-events: none; }
|
||||||
button { border: 1px solid #30363d; border-radius: 8px; background: #161b22; color: #e6edf3; padding: 7px 9px; cursor: pointer; }
|
button { border: 1px solid #30363d; border-radius: 8px; background: #161b22; color: #e6edf3; padding: 7px 9px; cursor: pointer; }
|
||||||
button:disabled, textarea:disabled { opacity: .5; cursor: not-allowed; }
|
button:disabled, textarea:disabled { opacity: .5; cursor: not-allowed; }
|
||||||
`;
|
`;
|
||||||
|
|||||||
@@ -56,7 +56,9 @@ export class SessionController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async send(text: string) {
|
async send(text: string) {
|
||||||
if (text.trim().startsWith("/")) return this.runCommand(text);
|
const trimmed = text.trim();
|
||||||
|
if (trimmed.startsWith("/")) return this.runCommand(text);
|
||||||
|
if (trimmed.startsWith("!")) return this.runShell(text);
|
||||||
const session = this.getState().selectedSession;
|
const session = this.getState().selectedSession;
|
||||||
if (!session) return;
|
if (!session) return;
|
||||||
this.setState({ messages: [...this.getState().messages, textMessage("user", text)] });
|
this.setState({ messages: [...this.getState().messages, textMessage("user", text)] });
|
||||||
@@ -67,6 +69,17 @@ export class SessionController {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async runShell(text: string) {
|
||||||
|
const session = this.getState().selectedSession;
|
||||||
|
if (!session) return;
|
||||||
|
this.setState({ messages: [...this.getState().messages, textMessage("user", text)] });
|
||||||
|
try {
|
||||||
|
await api.shell(session.id, text);
|
||||||
|
} catch (error) {
|
||||||
|
this.setState({ messages: [...this.getState().messages, textMessage("system", String(error))], error: String(error) });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async runCommand(text: string) {
|
async runCommand(text: string) {
|
||||||
const session = this.getState().selectedSession;
|
const session = this.getState().selectedSession;
|
||||||
if (!session) return;
|
if (!session) return;
|
||||||
@@ -143,6 +156,12 @@ export class SessionController {
|
|||||||
this.setState({ messages: appendPart(messages, "assistant", { type: "toolCall", toolName: event.toolName, summary: event.summary }) });
|
this.setState({ messages: appendPart(messages, "assistant", { type: "toolCall", toolName: event.toolName, summary: event.summary }) });
|
||||||
} else if (event.type === "tool.end") {
|
} else if (event.type === "tool.end") {
|
||||||
this.setState({ messages: [...messages, { role: "tool", parts: [{ type: "toolResult", toolName: event.toolName, text: event.text, isError: event.isError }] }] });
|
this.setState({ messages: [...messages, { role: "tool", parts: [{ type: "toolResult", toolName: event.toolName, text: event.text, isError: event.isError }] }] });
|
||||||
|
} else if (event.type === "shell.start") {
|
||||||
|
this.setState({ messages: [...messages, textMessage("bash", `$ ${event.command}${event.excludeFromContext ? "\n\nexcluded from context" : ""}`)] });
|
||||||
|
} else if (event.type === "shell.chunk") {
|
||||||
|
this.setState({ messages: appendShellChunk(messages, event.chunk) });
|
||||||
|
} else if (event.type === "shell.end") {
|
||||||
|
this.setState({ messages: finalizeShellMessage(messages, event) });
|
||||||
} else if (event.type === "status.update") {
|
} else if (event.type === "status.update") {
|
||||||
this.applyStatus(event.status);
|
this.applyStatus(event.status);
|
||||||
} else if (event.type === "activity.update") {
|
} else if (event.type === "activity.update") {
|
||||||
@@ -160,3 +179,26 @@ function appendPart(messages: ChatLine[], role: ChatLine["role"], part: ChatPart
|
|||||||
if (last?.role === role) return [...messages.slice(0, -1), { ...last, parts: [...last.parts, part] }];
|
if (last?.role === role) return [...messages.slice(0, -1), { ...last, parts: [...last.parts, part] }];
|
||||||
return [...messages, { role, parts: [part] }];
|
return [...messages, { role, parts: [part] }];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function appendShellChunk(messages: ChatLine[], chunk: string): ChatLine[] {
|
||||||
|
const last = messages.at(-1);
|
||||||
|
const lastPart = last?.parts.at(-1);
|
||||||
|
if (last?.role !== "bash" || lastPart?.type !== "text") return [...messages, textMessage("bash", chunk)];
|
||||||
|
const separator = lastPart.text.includes("\n\n") ? "" : "\n\n";
|
||||||
|
return [...messages.slice(0, -1), { ...last, parts: [...last.parts.slice(0, -1), { ...lastPart, text: lastPart.text + separator + chunk }] }];
|
||||||
|
}
|
||||||
|
|
||||||
|
function finalizeShellMessage(messages: ChatLine[], event: Extract<SessionUiEvent, { type: "shell.end" }>): ChatLine[] {
|
||||||
|
const last = messages.at(-1);
|
||||||
|
const lastPart = last?.parts.at(-1);
|
||||||
|
if (last?.role !== "bash" || lastPart?.type !== "text") return messages;
|
||||||
|
const notes: string[] = [];
|
||||||
|
if (!lastPart.text.includes("\n\n") && !event.output) notes.push("(no output)");
|
||||||
|
if (event.isError) notes.push(event.output ?? "Bash command failed");
|
||||||
|
if (event.exitCode != null) notes.push(`exit ${event.exitCode}`);
|
||||||
|
if (event.cancelled) notes.push("cancelled");
|
||||||
|
if (event.truncated) notes.push("output truncated");
|
||||||
|
if (event.fullOutputPath) notes.push(`full output: ${event.fullOutputPath}`);
|
||||||
|
if (!notes.length) return messages;
|
||||||
|
return [...messages.slice(0, -1), { ...last, parts: [...last.parts.slice(0, -1), { ...lastPart, text: `${lastPart.text}\n\n${notes.join("\n")}` }] }];
|
||||||
|
}
|
||||||
|
|||||||
@@ -4,6 +4,9 @@ export type SessionUiEvent =
|
|||||||
| { type: "assistant.delta"; text: string }
|
| { type: "assistant.delta"; text: string }
|
||||||
| { type: "tool.start"; toolName: string; summary: string }
|
| { type: "tool.start"; toolName: string; summary: string }
|
||||||
| { type: "tool.end"; toolName: string; text: string; isError: boolean }
|
| { type: "tool.end"; toolName: string; text: string; isError: boolean }
|
||||||
|
| { type: "shell.start"; command: string; excludeFromContext?: boolean }
|
||||||
|
| { type: "shell.chunk"; chunk: string }
|
||||||
|
| { type: "shell.end"; output?: string; exitCode?: number | null; cancelled?: boolean; truncated?: boolean; fullOutputPath?: string; isError?: boolean }
|
||||||
| { type: "status.update"; status: SessionStatus }
|
| { type: "status.update"; status: SessionStatus }
|
||||||
| { type: "activity.update"; activity: SessionActivity }
|
| { type: "activity.update"; activity: SessionActivity }
|
||||||
| { type: "command.output"; level: "info" | "success" | "error"; message: string }
|
| { type: "command.output"; level: "info" | "success" | "error"; message: string }
|
||||||
@@ -119,7 +122,7 @@ export class GlobalSessionSocket {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function isSessionUiEvent(event: any): event is SessionUiEvent {
|
function isSessionUiEvent(event: any): event is SessionUiEvent {
|
||||||
return ["assistant.delta", "tool.start", "tool.end", "status.update", "activity.update", "command.output", "session.error"].includes(event?.type);
|
return ["assistant.delta", "tool.start", "tool.end", "shell.start", "shell.chunk", "shell.end", "status.update", "activity.update", "command.output", "session.error"].includes(event?.type);
|
||||||
}
|
}
|
||||||
|
|
||||||
function isGlobalSessionEvent(event: unknown): event is Extract<SessionUiEvent, { type: "status.update" | "activity.update" }> {
|
function isGlobalSessionEvent(event: unknown): event is Extract<SessionUiEvent, { type: "status.update" | "activity.update" }> {
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ export async function registerSessionProxyRoutes(app: FastifyInstance, daemon =
|
|||||||
app.get<{ Params: { sessionId: string } }>("/api/sessions/:sessionId/status", (request, reply) => proxy(request, reply));
|
app.get<{ Params: { sessionId: string } }>("/api/sessions/:sessionId/status", (request, reply) => proxy(request, reply));
|
||||||
app.get<{ Params: { sessionId: string } }>("/api/sessions/:sessionId/commands", (request, reply) => proxy(request, reply));
|
app.get<{ Params: { sessionId: string } }>("/api/sessions/:sessionId/commands", (request, reply) => proxy(request, reply));
|
||||||
app.post<{ Params: { sessionId: string }; Body: { text: string } }>("/api/sessions/:sessionId/prompt", (request, reply) => proxy(request, reply));
|
app.post<{ Params: { sessionId: string }; Body: { text: string } }>("/api/sessions/:sessionId/prompt", (request, reply) => proxy(request, reply));
|
||||||
|
app.post<{ Params: { sessionId: string }; Body: { text: string } }>("/api/sessions/:sessionId/shell", (request, reply) => proxy(request, reply));
|
||||||
app.post<{ Params: { sessionId: string }; Body: { text: string } }>("/api/sessions/:sessionId/commands/run", (request, reply) => proxy(request, reply));
|
app.post<{ Params: { sessionId: string }; Body: { text: string } }>("/api/sessions/:sessionId/commands/run", (request, reply) => proxy(request, reply));
|
||||||
app.post<{ Params: { sessionId: string }; Body: { requestId: string; value: string } }>("/api/sessions/:sessionId/commands/respond", (request, reply) => proxy(request, reply));
|
app.post<{ Params: { sessionId: string }; Body: { requestId: string; value: string } }>("/api/sessions/:sessionId/commands/respond", (request, reply) => proxy(request, reply));
|
||||||
app.post<{ Params: { sessionId: string } }>("/api/sessions/:sessionId/abort", (request, reply) => proxy(request, reply));
|
app.post<{ Params: { sessionId: string } }>("/api/sessions/:sessionId/abort", (request, reply) => proxy(request, reply));
|
||||||
|
|||||||
@@ -101,6 +101,40 @@ export class PiSessionService {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async shell(sessionId: string, text: string): Promise<void> {
|
||||||
|
const active = await this.getActive(sessionId);
|
||||||
|
const { session } = active.runtime;
|
||||||
|
const isExcluded = text.startsWith("!!");
|
||||||
|
const command = (isExcluded ? text.slice(2) : text.slice(1)).trim();
|
||||||
|
if (!command) throw new Error("Usage: !<shell command>");
|
||||||
|
if (session.isBashRunning) throw new Error("A bash command is already running");
|
||||||
|
|
||||||
|
this.publishActivity(session, "running bash", "active", command);
|
||||||
|
this.events.publish(session.sessionId, { type: "shell.start", command, excludeFromContext: isExcluded });
|
||||||
|
void session.executeBash(command, (chunk) => {
|
||||||
|
this.events.publish(session.sessionId, { type: "shell.chunk", chunk });
|
||||||
|
this.publishActivity(session, "running bash", "active", command);
|
||||||
|
this.publishStatus(session);
|
||||||
|
}, { excludeFromContext: isExcluded }).then((result) => {
|
||||||
|
this.events.publish(session.sessionId, {
|
||||||
|
type: "shell.end",
|
||||||
|
output: result.output,
|
||||||
|
exitCode: result.exitCode,
|
||||||
|
cancelled: result.cancelled,
|
||||||
|
truncated: result.truncated,
|
||||||
|
fullOutputPath: result.fullOutputPath,
|
||||||
|
});
|
||||||
|
this.publishActivity(session, "bash complete", result.exitCode === 0 ? "idle" : "error", command);
|
||||||
|
this.publishStatus(session);
|
||||||
|
}).catch((error) => {
|
||||||
|
const message = error instanceof Error ? error.message : String(error);
|
||||||
|
this.events.publish(session.sessionId, { type: "shell.end", output: message, isError: true });
|
||||||
|
this.events.publish(session.sessionId, { type: "session.error", message });
|
||||||
|
this.publishActivity(session, "bash failed", "error", message);
|
||||||
|
this.publishStatus(session);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
async runCommand(sessionId: string, text: string): Promise<ClientCommandResult> {
|
async runCommand(sessionId: string, text: string): Promise<ClientCommandResult> {
|
||||||
return this.commandService.run(sessionId, text);
|
return this.commandService.run(sessionId, text);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -49,6 +49,15 @@ export async function registerSessionRoutes(app: FastifyInstance, sessions: PiSe
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
app.post<{ Params: { sessionId: string }; Body: { text: string } }>(`${prefix}/sessions/:sessionId/shell`, async (request, reply) => {
|
||||||
|
try {
|
||||||
|
await sessions.shell(request.params.sessionId, request.body.text);
|
||||||
|
return { accepted: true };
|
||||||
|
} catch (error) {
|
||||||
|
return reply.code(400).send({ error: error instanceof Error ? error.message : String(error) });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
app.post<{ Params: { sessionId: string }; Body: { text: string } }>(`${prefix}/sessions/:sessionId/commands/run`, async (request, reply) => {
|
app.post<{ Params: { sessionId: string }; Body: { text: string } }>(`${prefix}/sessions/:sessionId/commands/run`, async (request, reply) => {
|
||||||
try {
|
try {
|
||||||
return await sessions.runCommand(request.params.sessionId, request.body.text);
|
return await sessions.runCommand(request.params.sessionId, request.body.text);
|
||||||
|
|||||||
Reference in New Issue
Block a user