Reimplement compose and add tiling windows

This commit is contained in:
2026-03-12 22:03:30 +00:00
parent 79766d279d
commit 6ceff63b71
126 changed files with 5111 additions and 10796 deletions

View File

@@ -0,0 +1,321 @@
(function () {
if (window.GIAComposePanelSend) {
return;
}
const core = window.GIAComposePanelCore;
if (!core) {
return;
}
const createController = function (config) {
const panel = config.panel;
const panelId = config.panelId;
const state = config.state;
const thread = config.thread;
const form = config.form;
const textarea = config.textarea;
const statusBox = config.statusBox;
const manualConfirm = config.manualConfirm;
const armInput = config.armInput;
const confirmInput = config.confirmInput;
const sendButton = config.sendButton;
const sendCapable = config.sendCapable;
const csrfToken = config.csrfToken;
const queryParams = config.queryParams;
const poll = config.poll;
const clearReplyTarget = config.clearReplyTarget;
const autosize = config.autosize;
const flashCompose = config.flashCompose;
const setStatus = config.setStatus;
let transientCancelButton = null;
let persistentCancelWrap = null;
const postFormJson = async function (url, params) {
const response = await fetch(url, {
method: "POST",
credentials: "same-origin",
headers: {
"X-CSRFToken": csrfToken,
"Content-Type": "application/x-www-form-urlencoded",
Accept: "application/json",
},
body: params.toString(),
});
if (!response.ok) {
throw new Error("Request failed");
}
return response.json();
};
const cancelSendRequest = function (commandId) {
return postFormJson(
String(panel.dataset.cancelSendUrl || ""),
queryParams({ command_id: String(commandId || "") })
);
};
const hideTransientCancelButton = function () {
if (!transientCancelButton) {
return;
}
transientCancelButton.remove();
transientCancelButton = null;
};
const showTransientCancelButton = function () {
if (!statusBox || transientCancelButton) {
return;
}
transientCancelButton = core.createNode(
"button",
"button is-danger is-light is-small compose-cancel-send-btn",
"Cancel Send"
);
transientCancelButton.type = "button";
transientCancelButton.addEventListener("click", async function () {
try {
await cancelSendRequest("");
} catch (_err) {
// Ignore cancel failures.
} finally {
hideTransientCancelButton();
}
});
statusBox.appendChild(transientCancelButton);
};
const hidePersistentCancelButton = function () {
if (!persistentCancelWrap) {
return;
}
persistentCancelWrap.remove();
persistentCancelWrap = null;
};
const stopPendingCommandPolling = function () {
if (state.pendingCommandPoll) {
clearInterval(state.pendingCommandPoll);
state.pendingCommandPoll = null;
}
state.pendingCommandId = "";
state.pendingCommandAttempts = 0;
state.pendingCommandStartedAt = 0;
state.pendingCommandInFlight = false;
};
const showPersistentCancelButton = function (commandId) {
hidePersistentCancelButton();
if (!statusBox) {
return;
}
persistentCancelWrap = core.createNode("div", "compose-persistent-cancel");
const button = core.createNode(
"button",
"button is-danger is-light is-small compose-persistent-cancel-btn",
"Cancel Queued Send"
);
button.type = "button";
button.addEventListener("click", async function () {
try {
await cancelSendRequest(commandId);
stopPendingCommandPolling();
hidePersistentCancelButton();
setStatus("Send cancelled.", "warning");
await poll(true);
} catch (_err) {
hidePersistentCancelButton();
}
});
persistentCancelWrap.appendChild(button);
statusBox.appendChild(persistentCancelWrap);
};
const pollPendingCommandResult = async function (commandId) {
const url = new URL(
String(panel.dataset.commandResultUrl || ""),
window.location.origin
);
url.searchParams.set("service", thread.dataset.service || "");
url.searchParams.set("command_id", commandId);
url.searchParams.set("format", "json");
const response = await fetch(url.toString(), {
credentials: "same-origin",
headers: { "HX-Request": "true" },
});
if (!response.ok || response.status === 204) {
return null;
}
return response.json();
};
const startPendingCommandPolling = function (commandId) {
if (!commandId) {
return;
}
stopPendingCommandPolling();
state.pendingCommandId = commandId;
state.pendingCommandStartedAt = Date.now();
showPersistentCancelButton(commandId);
state.pendingCommandPoll = setInterval(async function () {
if (state.pendingCommandInFlight) {
return;
}
state.pendingCommandAttempts += 1;
if (
state.pendingCommandAttempts > 14
|| (Date.now() - state.pendingCommandStartedAt) > 45000
) {
stopPendingCommandPolling();
hidePersistentCancelButton();
setStatus(
"Send timed out waiting for runtime result. Please retry.",
"warning"
);
return;
}
try {
state.pendingCommandInFlight = true;
const payload = await pollPendingCommandResult(commandId);
if (!payload || payload.pending !== false) {
return;
}
const result = payload.result || {};
stopPendingCommandPolling();
hidePersistentCancelButton();
if (result.ok) {
setStatus("", "success");
textarea.value = "";
clearReplyTarget();
autosize();
flashCompose("is-send-success");
await poll(true);
return;
}
setStatus(String(result.error || "Send failed."), "danger");
flashCompose("is-send-fail");
await poll(true);
} catch (_err) {
// Ignore transient failures; the next poll can recover.
} finally {
state.pendingCommandInFlight = false;
}
}, 3500);
};
const updateManualSafety = function () {
const confirmed = !!(manualConfirm && manualConfirm.checked);
if (armInput) {
armInput.value = confirmed ? "1" : "0";
}
if (confirmInput) {
confirmInput.value = confirmed ? "1" : "0";
}
if (sendButton) {
sendButton.disabled = !sendCapable || !confirmed;
}
};
const bindSendEvents = function () {
textarea.addEventListener("keydown", function (event) {
if (event.key !== "Enter" || event.shiftKey) {
return;
}
event.preventDefault();
if (sendButton && sendButton.disabled) {
setStatus("Enable send confirmation before sending.", "warning");
return;
}
form.requestSubmit();
});
form.addEventListener("submit", function () {
if (sendButton && sendButton.disabled) {
return;
}
showTransientCancelButton();
});
form.addEventListener("htmx:afterRequest", function () {
hideTransientCancelButton();
textarea.focus();
});
};
const bindDocumentEvents = function () {
state.eventHandler = function (event) {
const detail = (event && event.detail) || {};
const sourcePanelId = String(detail.panel_id || "");
if (sourcePanelId && sourcePanelId !== panelId) {
return;
}
poll(true);
};
document.body.addEventListener("composeMessageSent", state.eventHandler);
state.sendResultHandler = function (event) {
const detail = (event && event.detail) || {};
const sourcePanelId = String(detail.panel_id || "");
if (sourcePanelId && sourcePanelId !== panelId) {
return;
}
hideTransientCancelButton();
if (detail.ok) {
flashCompose("is-send-success");
textarea.value = "";
clearReplyTarget();
autosize();
poll(true);
} else {
flashCompose("is-send-fail");
if (detail.message) {
setStatus(detail.message, detail.level || "danger");
}
}
textarea.focus();
};
document.body.addEventListener("composeSendResult", state.sendResultHandler);
state.commandIdHandler = function (event) {
const detail = (event && event.detail) || {};
const commandId = String(
detail.command_id
|| (detail.composeSendCommandId && detail.composeSendCommandId.command_id)
|| ""
).trim();
if (commandId) {
startPendingCommandPolling(commandId);
}
};
document.body.addEventListener(
"composeSendCommandId",
state.commandIdHandler
);
};
const init = function () {
bindSendEvents();
bindDocumentEvents();
if (manualConfirm) {
manualConfirm.addEventListener("change", updateManualSafety);
manualConfirm.dispatchEvent(new Event("change"));
} else {
updateManualSafety();
}
};
return {
init: init,
resetForContextSwitch: function () {
stopPendingCommandPolling();
hidePersistentCancelButton();
hideTransientCancelButton();
},
};
};
window.GIAComposePanelSend = {
createController: createController,
};
})();