Custom Commands: Typen (Hintergrund/Vordergrund/Nachricht) mit Output-Modal
This commit is contained in:
1 parent
d83a6f60ef
commit
ab234fde69
8 files changed
+241
-36
No files matched your search
@@ -159,6 +159,7 @@ public static class SessionEndpoint
|
||||
|
||||
var body = await JsonSerializer.DeserializeAsync<JsonElement>(ctx.Request.Body);
|
||||
var command = body.TryGetProperty("command", out var c) ? c.GetString() : null;
|
||||
var mode = body.TryGetProperty("mode", out var m) ? m.GetString() ?? "background" : "background";
|
||||
|
||||
if (string.IsNullOrWhiteSpace(command))
|
||||
{
|
||||
@@ -169,35 +170,97 @@ public static class SessionEndpoint
|
||||
|
||||
var workingDir = !string.IsNullOrWhiteSpace(session.Workspace) ? session.Workspace : "/tmp";
|
||||
|
||||
try
|
||||
if (mode == "foreground")
|
||||
{
|
||||
var scriptPath = $"/tmp/gott-exec-{Guid.NewGuid():N}.sh";
|
||||
await System.IO.File.WriteAllTextAsync(scriptPath, $"#!/bin/bash\ncd {workingDir}\n{command}\nrm -f {scriptPath}\n");
|
||||
|
||||
var psi = new ProcessStartInfo
|
||||
// Synchron ausführen, auf Ergebnis warten
|
||||
try
|
||||
{
|
||||
FileName = "systemd-run",
|
||||
Arguments = $"--scope --quiet bash {scriptPath}",
|
||||
WorkingDirectory = workingDir,
|
||||
UseShellExecute = false,
|
||||
RedirectStandardOutput = true,
|
||||
RedirectStandardError = true,
|
||||
CreateNoWindow = true
|
||||
};
|
||||
var psi = new ProcessStartInfo
|
||||
{
|
||||
FileName = "/bin/bash",
|
||||
WorkingDirectory = workingDir,
|
||||
RedirectStandardInput = true,
|
||||
RedirectStandardOutput = true,
|
||||
RedirectStandardError = true,
|
||||
UseShellExecute = false,
|
||||
CreateNoWindow = true
|
||||
};
|
||||
|
||||
var process = Process.Start(psi);
|
||||
if (process != null)
|
||||
using var proc = System.Diagnostics.Process.Start(psi);
|
||||
if (proc == null)
|
||||
{
|
||||
ctx.Response.StatusCode = 500;
|
||||
await ctx.Response.WriteAsJsonAsync(new { error = "Prozess konnte nicht gestartet werden" });
|
||||
return;
|
||||
}
|
||||
|
||||
await proc.StandardInput.WriteAsync(command);
|
||||
proc.StandardInput.Close();
|
||||
|
||||
using var cts = new System.Threading.CancellationTokenSource(TimeSpan.FromSeconds(30));
|
||||
try
|
||||
{
|
||||
await proc.WaitForExitAsync(cts.Token);
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
proc.Kill(entireProcessTree: true);
|
||||
await ctx.Response.WriteAsJsonAsync(new { status = "timeout", output = "Timeout nach 30 Sekunden", exitCode = -1 });
|
||||
return;
|
||||
}
|
||||
|
||||
var stdout = await proc.StandardOutput.ReadToEndAsync();
|
||||
var stderr = await proc.StandardError.ReadToEndAsync();
|
||||
|
||||
var output = new System.Text.StringBuilder();
|
||||
if (!string.IsNullOrEmpty(stdout)) output.Append(stdout);
|
||||
if (!string.IsNullOrEmpty(stderr))
|
||||
{
|
||||
if (output.Length > 0) output.Append("\n--- stderr ---\n");
|
||||
output.Append(stderr);
|
||||
}
|
||||
|
||||
await ctx.Response.WriteAsJsonAsync(new { status = "completed", output = output.ToString(), exitCode = proc.ExitCode });
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
process.WaitForExit(500);
|
||||
ctx.Response.StatusCode = 500;
|
||||
await ctx.Response.WriteAsJsonAsync(new { error = ex.Message });
|
||||
}
|
||||
|
||||
ctx.Response.StatusCode = 202;
|
||||
await ctx.Response.WriteAsJsonAsync(new { status = "started", command = command });
|
||||
}
|
||||
catch (Exception ex)
|
||||
else
|
||||
{
|
||||
ctx.Response.StatusCode = 500;
|
||||
await ctx.Response.WriteAsJsonAsync(new { error = ex.Message });
|
||||
// Fire-and-forget via systemd-run
|
||||
try
|
||||
{
|
||||
var scriptPath = $"/tmp/gott-exec-{Guid.NewGuid():N}.sh";
|
||||
await System.IO.File.WriteAllTextAsync(scriptPath, $"#!/bin/bash\ncd {workingDir}\n{command}\nrm -f {scriptPath}\n");
|
||||
|
||||
var psi = new ProcessStartInfo
|
||||
{
|
||||
FileName = "systemd-run",
|
||||
Arguments = $"--scope --quiet bash {scriptPath}",
|
||||
WorkingDirectory = workingDir,
|
||||
UseShellExecute = false,
|
||||
RedirectStandardOutput = true,
|
||||
RedirectStandardError = true,
|
||||
CreateNoWindow = true
|
||||
};
|
||||
|
||||
var process = Process.Start(psi);
|
||||
if (process != null)
|
||||
{
|
||||
process.WaitForExit(500);
|
||||
}
|
||||
|
||||
ctx.Response.StatusCode = 202;
|
||||
await ctx.Response.WriteAsJsonAsync(new { status = "started", command = command });
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
ctx.Response.StatusCode = 500;
|
||||
await ctx.Response.WriteAsJsonAsync(new { error = ex.Message });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -449,3 +449,56 @@
|
||||
opacity: var(--semi-transparent);
|
||||
padding: 2em;
|
||||
}
|
||||
|
||||
/* Command Output Modal */
|
||||
|
||||
.cmd-output-modal {
|
||||
max-width: 800px;
|
||||
}
|
||||
|
||||
.cmd-output-meta {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--badge-spacing);
|
||||
padding-bottom: var(--box-spacing);
|
||||
border-bottom: var(--border-width) solid var(--f1-border);
|
||||
margin-bottom: var(--box-spacing);
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.cmd-output-meta code {
|
||||
flex: 1;
|
||||
font-family: var(--mono);
|
||||
font-size: var(--mono-size);
|
||||
opacity: var(--semi-transparent);
|
||||
}
|
||||
|
||||
.cmd-exit-code {
|
||||
padding: var(--badge-padding);
|
||||
border-radius: var(--border-radius);
|
||||
font-size: var(--font-size);
|
||||
background: var(--f3-bg);
|
||||
border: var(--border-width) solid var(--f3-border);
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.cmd-exit-error {
|
||||
color: var(--dangerous);
|
||||
border-color: var(--dangerous);
|
||||
}
|
||||
|
||||
.cmd-output-content {
|
||||
font-family: var(--mono);
|
||||
font-size: var(--mono-size);
|
||||
white-space: pre-wrap;
|
||||
word-break: break-word;
|
||||
margin: 0;
|
||||
max-height: 60vh;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
/* Message-Typ Button */
|
||||
|
||||
.cmd-type-message {
|
||||
font-style: italic;
|
||||
}
|
||||
@@ -75,6 +75,11 @@
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.custom-command-fields .cmd-type {
|
||||
flex: 0 0 auto;
|
||||
width: 10em;
|
||||
}
|
||||
|
||||
.custom-command-fields .cmd-name {
|
||||
flex: 2;
|
||||
}
|
||||
|
||||
+1
-1
@@ -86,7 +86,7 @@
|
||||
</div>
|
||||
<label>Kontext</label>
|
||||
<textarea id="sessionContext" rows="4" placeholder="Zusätzlicher Kontext für den System-Prompt (optional)"></textarea>
|
||||
<label>Custom-Befehle</label>
|
||||
<label>Benutzerdefinierte Befehle</label>
|
||||
<div class="custom-commands" id="customCommandsList"></div>
|
||||
<button type="button" id="addCommandBtn" class="add-cmd-btn">+ Befehl hinzufügen</button>
|
||||
<div class="form-actions">
|
||||
|
||||
+5
-3
@@ -104,12 +104,14 @@ async function apiUpdateSession(sessionId, data) {
|
||||
return resp.json();
|
||||
}
|
||||
|
||||
async function apiExecCommand(sessionId, command) {
|
||||
return fetch(`/api/sessions/${sessionId}/exec`, {
|
||||
async function apiExecCommand(sessionId, command, mode = 'background') {
|
||||
const resp = await fetch(`/api/sessions/${sessionId}/exec`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ command })
|
||||
body: JSON.stringify({ command, mode })
|
||||
});
|
||||
if (mode === 'foreground') return resp.json();
|
||||
return resp;
|
||||
}
|
||||
|
||||
async function apiGetSystemPrompt(sessionId) {
|
||||
|
||||
@@ -65,22 +65,37 @@ registerPage('chat', {
|
||||
try { cmds = JSON.parse(session.customCommands || '[]') || []; } catch(e) {}
|
||||
cmds.forEach(cmd => {
|
||||
if (!cmd.name || !cmd.command) return;
|
||||
const type = cmd.type || 'background';
|
||||
const btn = document.createElement('button');
|
||||
btn.className = 'custom-cmd-btn';
|
||||
if (type === 'message') btn.classList.add('cmd-type-message');
|
||||
btn.textContent = cmd.name;
|
||||
btn.title = cmd.command;
|
||||
btn.title = type === 'message' ? cmd.command : `[${type}] ${cmd.command}`;
|
||||
btn.addEventListener('click', async () => {
|
||||
if (type === 'message') {
|
||||
// Nachricht in den Chat senden
|
||||
const inputEl = document.getElementById('input');
|
||||
if (inputEl) {
|
||||
inputEl.value = cmd.command;
|
||||
inputEl.dispatchEvent(new Event('input'));
|
||||
}
|
||||
sendMessage();
|
||||
return;
|
||||
}
|
||||
btn.disabled = true;
|
||||
btn.classList.add('running');
|
||||
try {
|
||||
await apiExecCommand(sessionId, cmd.command);
|
||||
if (type === 'foreground') {
|
||||
const result = await apiExecCommand(sessionId, cmd.command, 'foreground');
|
||||
showCommandOutputModal(cmd.name, cmd.command, result);
|
||||
} else {
|
||||
await apiExecCommand(sessionId, cmd.command, 'background');
|
||||
}
|
||||
} catch(e) {
|
||||
console.error('Exec failed:', e);
|
||||
}
|
||||
setTimeout(() => {
|
||||
btn.disabled = false;
|
||||
btn.classList.remove('running');
|
||||
}, 1000);
|
||||
btn.disabled = false;
|
||||
btn.classList.remove('running');
|
||||
});
|
||||
cmdBar.appendChild(btn);
|
||||
});
|
||||
|
||||
@@ -3,16 +3,27 @@
|
||||
let _sessionImageData = null;
|
||||
let _sessionPasteHandler = null;
|
||||
|
||||
function _createCommandRow(container, name = '', command = '') {
|
||||
function _createCommandRow(container, name = '', command = '', type = 'background') {
|
||||
const item = document.createElement('div');
|
||||
item.className = 'custom-command-item';
|
||||
item.innerHTML = `
|
||||
<div class="custom-command-fields">
|
||||
<select class="cmd-type">
|
||||
<option value="background">Hintergrund</option>
|
||||
<option value="foreground">Vordergrund</option>
|
||||
<option value="message">Nachricht</option>
|
||||
</select>
|
||||
<input type="text" class="cmd-name" placeholder="Name (z.B. Deploy)" value="${_escAttr(name)}">
|
||||
<input type="text" class="cmd-command" placeholder="Befehl (z.B. ./deploy.sh)" value="${_escAttr(command)}">
|
||||
<input type="text" class="cmd-command" placeholder="${type === 'message' ? 'Nachricht...' : 'Befehl (z.B. ./deploy.sh)'}" value="${_escAttr(command)}">
|
||||
</div>
|
||||
<button type="button" class="remove-cmd-btn icon-btn"><i class="fa fa-times"></i></button>
|
||||
`;
|
||||
const typeSelect = item.querySelector('.cmd-type');
|
||||
typeSelect.value = type;
|
||||
typeSelect.addEventListener('change', () => {
|
||||
const cmdInput = item.querySelector('.cmd-command');
|
||||
cmdInput.placeholder = typeSelect.value === 'message' ? 'Nachricht...' : 'Befehl (z.B. ./deploy.sh)';
|
||||
});
|
||||
item.querySelector('.remove-cmd-btn').addEventListener('click', () => item.remove());
|
||||
container.appendChild(item);
|
||||
}
|
||||
@@ -27,7 +38,8 @@ function _getCommandsFromUI(container) {
|
||||
for (const item of items) {
|
||||
const name = item.querySelector('.cmd-name').value.trim();
|
||||
const command = item.querySelector('.cmd-command').value.trim();
|
||||
if (name || command) commands.push({ name, command });
|
||||
const type = item.querySelector('.cmd-type').value;
|
||||
if (name || command) commands.push({ name, command, type });
|
||||
}
|
||||
return commands.length > 0 ? commands : null;
|
||||
}
|
||||
@@ -83,7 +95,7 @@ registerPage('session', {
|
||||
? JSON.parse(session.customCommands)
|
||||
: session.customCommands;
|
||||
for (const cmd of cmds) {
|
||||
_createCommandRow(commandsList, cmd.name || '', cmd.command || '');
|
||||
_createCommandRow(commandsList, cmd.name || '', cmd.command || '', cmd.type || 'background');
|
||||
}
|
||||
} catch (e) { /* ignore parse errors */ }
|
||||
}
|
||||
|
||||
@@ -461,6 +461,61 @@ async function showSystemPromptModal(sessionId) {
|
||||
}
|
||||
}
|
||||
|
||||
// ── Command Output Modal ──
|
||||
|
||||
function showCommandOutputModal(name, command, result) {
|
||||
const overlay = document.createElement('div');
|
||||
overlay.className = 'sp-overlay';
|
||||
|
||||
const modal = document.createElement('div');
|
||||
modal.className = 'sp-modal cmd-output-modal';
|
||||
|
||||
// Header
|
||||
const header = document.createElement('div');
|
||||
header.className = 'sp-header';
|
||||
const title = document.createElement('span');
|
||||
title.textContent = name;
|
||||
const closeBtn = document.createElement('button');
|
||||
closeBtn.innerHTML = '<i class="fa fa-times"></i>';
|
||||
closeBtn.className = 'sp-close icon-btn';
|
||||
header.appendChild(title);
|
||||
header.appendChild(closeBtn);
|
||||
|
||||
// Body
|
||||
const body = document.createElement('div');
|
||||
body.className = 'sp-body';
|
||||
|
||||
const exitCode = result.exitCode != null ? result.exitCode : '?';
|
||||
const isError = exitCode !== 0;
|
||||
const statusText = result.status === 'timeout' ? 'Timeout' : (isError ? `Exit ${exitCode}` : `Exit ${exitCode}`);
|
||||
|
||||
const meta = document.createElement('div');
|
||||
meta.className = 'cmd-output-meta';
|
||||
meta.innerHTML = `<code>${esc(command)}</code> <span class="cmd-exit-code${isError ? ' cmd-exit-error' : ''}">${esc(statusText)}</span>`;
|
||||
body.appendChild(meta);
|
||||
|
||||
const output = document.createElement('pre');
|
||||
output.className = 'cmd-output-content';
|
||||
output.textContent = result.output || '(kein Output)';
|
||||
body.appendChild(output);
|
||||
|
||||
modal.appendChild(header);
|
||||
modal.appendChild(body);
|
||||
overlay.appendChild(modal);
|
||||
document.body.appendChild(overlay);
|
||||
|
||||
// Schließen
|
||||
const close = () => overlay.remove();
|
||||
closeBtn.addEventListener('click', close);
|
||||
overlay.addEventListener('click', (e) => {
|
||||
if (e.target === overlay) close();
|
||||
});
|
||||
const onKey = (e) => {
|
||||
if (e.key === 'Escape') { close(); document.removeEventListener('keydown', onKey); }
|
||||
};
|
||||
document.addEventListener('keydown', onKey);
|
||||
}
|
||||
|
||||
// ── Meta Bar ──
|
||||
|
||||
function setMeta(text) {
|
||||
|
||||
Reference in new issue
Block a user