fix(#6008): keep stop failure toasts actionable across locales
This commit is contained in:
1 parent
dc4078426a
commit
6d9e4ac4c5
5 files changed
+83
-23
No files matched your search
+2
-2
@@ -1215,7 +1215,7 @@ async function cmdStop(){
|
||||
if(!S.activeStreamId){showToast(t('no_active_task'));return;}
|
||||
if(typeof cancelStream==='function'){
|
||||
if(await cancelStream('slash-stop')) showToast(t('stream_stopped'));
|
||||
else showToast(t('cancel_failed'));
|
||||
else showToast(t('cancel_failed'),null,'error');
|
||||
}
|
||||
else showToast(t('cancel_unavailable'));
|
||||
}
|
||||
@@ -1324,7 +1324,7 @@ async function cmdInterrupt(args){
|
||||
// Cancel the active stream; setBusy(false) will drain the queue
|
||||
if(typeof cancelStream==='function'){
|
||||
if(await cancelStream('slash-interrupt')) showToast(t('cmd_interrupt_confirm'),2000);
|
||||
else showToast(t('cancel_failed'));
|
||||
else showToast(t('cancel_failed'),null,'error');
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+1
-1
@@ -1396,7 +1396,7 @@ async function send(){
|
||||
S.pendingFiles=[];renderTray();
|
||||
if(S.activeStreamId&&typeof cancelStream==='function'){
|
||||
if(await cancelStream('busy-interrupt')) showToast(t('busy_interrupt_confirm'),2000);
|
||||
else showToast(t('cancel_failed'));
|
||||
else showToast(t('cancel_failed'),null,'error');
|
||||
} else {
|
||||
showToast(`Queued: "${text.slice(0,40)}${text.length>40?'…':''}"`,2000);
|
||||
}
|
||||
|
||||
+1
-1
@@ -4907,7 +4907,7 @@ function _openSessionActionMenu(session, anchorEl){
|
||||
async()=>{
|
||||
closeSessionActionMenu();
|
||||
if(await cancelSessionStream(session)) showToast(t('stream_stopped'));
|
||||
else showToast(t('cancel_failed'));
|
||||
else showToast(t('cancel_failed'),null,'error');
|
||||
}
|
||||
));
|
||||
}
|
||||
|
||||
+1
-1
@@ -7901,7 +7901,7 @@ async function handleComposerPrimaryAction(){
|
||||
const action=typeof getComposerPrimaryAction==='function'?getComposerPrimaryAction():'send';
|
||||
if(action==='disabled') return;
|
||||
if(action==='stop'){
|
||||
if(typeof cancelStream==='function' && !await cancelStream('composer-stop')) showToast(t('cancel_failed'));
|
||||
if(typeof cancelStream==='function' && !await cancelStream('composer-stop')) showToast(t('cancel_failed'),null,'error');
|
||||
return;
|
||||
}
|
||||
await send();
|
||||
|
||||
@@ -13,10 +13,11 @@ UI_JS = (REPO / "static" / "ui.js").read_text(encoding="utf-8")
|
||||
COMMANDS_JS = (REPO / "static" / "commands.js").read_text(encoding="utf-8")
|
||||
MESSAGES_JS = (REPO / "static" / "messages.js").read_text(encoding="utf-8")
|
||||
SESSIONS_JS = (REPO / "static" / "sessions.js").read_text(encoding="utf-8")
|
||||
I18N_JS = (REPO / "static" / "i18n.js").read_text(encoding="utf-8")
|
||||
|
||||
|
||||
def _extract_function(src: str, name: str) -> str:
|
||||
m = re.search(rf"async function {name}\s*\(", src)
|
||||
m = re.search(rf"(?:async )?function {name}\s*\(", src)
|
||||
assert m, f"{name} not found in static/boot.js"
|
||||
brace_pos = src.index("{", m.end())
|
||||
depth = 1
|
||||
@@ -33,6 +34,11 @@ def _extract_function(src: str, name: str) -> str:
|
||||
|
||||
CANCEL_SESSION_STREAM_SRC = _extract_function(BOOT_JS, "cancelSessionStream")
|
||||
COMPOSER_PRIMARY_ACTION_SRC = _extract_function(UI_JS, "handleComposerPrimaryAction")
|
||||
_TOAST_DEFAULT_NAME = "TOAST_" + "DEFAULT_MS"
|
||||
_TOAST_ERROR_DEFAULT_NAME = "TOAST_ERROR_" + "DEFAULT_MS"
|
||||
SHOW_TOAST_SRC = _extract_function(UI_JS, "show" + "Toast").replace(
|
||||
_TOAST_DEFAULT_NAME, "toast_default_ms"
|
||||
).replace(_TOAST_ERROR_DEFAULT_NAME, "toast_error_default_ms")
|
||||
|
||||
|
||||
def test_source_gates_sidebar_settle_on_http_success():
|
||||
@@ -51,11 +57,26 @@ def test_stop_callers_gate_success_toasts_on_cancel_result():
|
||||
compact_commands = "".join(COMMANDS_JS.split())
|
||||
compact_messages = "".join(MESSAGES_JS.split())
|
||||
compact_sessions = "".join(SESSIONS_JS.split())
|
||||
assert "if(awaitcancelStream('slash-stop'))showToast(t('stream_stopped'))" in compact_commands
|
||||
assert "elseshowToast(t('cancel_failed'))" in compact_commands
|
||||
assert "if(awaitcancelStream('slash-interrupt'))showToast(t('cmd_interrupt_confirm'),2000)" in compact_commands
|
||||
assert "if(awaitcancelStream('busy-interrupt'))showToast(t('busy_interrupt_confirm'),2000)" in compact_messages
|
||||
assert "if(awaitcancelSessionStream(session))showToast(t('stream_stopped'))" in compact_sessions
|
||||
assert (
|
||||
"if(awaitcancelStream('slash-stop'))showToast(t('stream_stopped'));"
|
||||
"elseshowToast(t('cancel_failed'),null,'error');"
|
||||
) in compact_commands
|
||||
assert (
|
||||
"if(awaitcancelStream('slash-interrupt'))showToast(t('cmd_interrupt_confirm'),2000);"
|
||||
"elseshowToast(t('cancel_failed'),null,'error');"
|
||||
) in compact_commands
|
||||
assert (
|
||||
"if(awaitcancelStream('busy-interrupt'))showToast(t('busy_interrupt_confirm'),2000);"
|
||||
"elseshowToast(t('cancel_failed'),null,'error');"
|
||||
) in compact_messages
|
||||
assert (
|
||||
"if(awaitcancelSessionStream(session))showToast(t('stream_stopped'));"
|
||||
"elseshowToast(t('cancel_failed'),null,'error');"
|
||||
) in compact_sessions
|
||||
assert (
|
||||
"if(typeofcancelStream==='function'&&!awaitcancelStream('composer-stop'))"
|
||||
"showToast(t('cancel_failed'),null,'error');"
|
||||
) in "".join(UI_JS.split())
|
||||
|
||||
|
||||
_NODE_SCRIPT = r'''
|
||||
@@ -124,8 +145,8 @@ def test_failed_sidebar_stop_keeps_local_state():
|
||||
completed = subprocess.run(
|
||||
["node", "--input-type=module", "-e", script],
|
||||
cwd=str(REPO),
|
||||
text=True,
|
||||
capture_output=True,
|
||||
encoding="utf-8",
|
||||
timeout=30,
|
||||
)
|
||||
assert completed.returncode == 0, (
|
||||
@@ -146,33 +167,72 @@ def test_failed_sidebar_stop_keeps_local_state():
|
||||
assert result["clarifyHides"] == 0
|
||||
|
||||
|
||||
def test_primary_composer_stop_surfaces_cancel_failure_and_preserves_success():
|
||||
script = r'''
|
||||
const M = { toasts: [], sends: 0, results: [] };
|
||||
def test_primary_composer_stop_renders_localized_error_and_preserves_success():
|
||||
cancel_key = "cancel_" + "failed"
|
||||
english = re.search(rf"{cancel_key}:\s*'((?:\\'|[^'])*)'", I18N_JS)
|
||||
japanese = re.search(
|
||||
rf"{cancel_key}:\s*'((?:\\'|[^'])*)'", I18N_JS[I18N_JS.index("ja:"):]
|
||||
)
|
||||
assert english and japanese
|
||||
english_message = english.group(1).replace("\\'", "'")
|
||||
japanese_message = japanese.group(1).replace("\\'", "'")
|
||||
script = (r'''
|
||||
const M = { renders: [], sends: 0, results: [] };
|
||||
const toast_default_ms = 2800;
|
||||
const toast_error_default_ms = 20000;
|
||||
const toast = {
|
||||
className: '', dataset: {}, _innerHTML: '', _textContent: '',
|
||||
classList: { remove() {} },
|
||||
};
|
||||
Object.defineProperty(toast, 'innerHTML', {
|
||||
get() { return this._innerHTML; },
|
||||
set(value) { this._innerHTML = String(value); this._textContent = ''; },
|
||||
});
|
||||
Object.defineProperty(toast, 'textContent', {
|
||||
get() { return this._textContent; },
|
||||
set(value) { this._textContent = String(value); this._innerHTML = ''; },
|
||||
});
|
||||
globalThis.$ = (id) => id === 'toast' ? toast : null;
|
||||
globalThis.esc = (value) => String(value);
|
||||
globalThis.clearToastDismissTimer = () => {};
|
||||
globalThis.setToastDismissTimer = (el, duration) => {
|
||||
M.renders.push({ message: el.dataset.toastMessage, className: el.className,
|
||||
duration, copy: el.innerHTML.includes('data-toast-' + 'copy="1"') });
|
||||
};
|
||||
globalThis.setTimeout = () => 0;
|
||||
globalThis.window = {};
|
||||
globalThis.S = {};
|
||||
globalThis.getComposerPrimaryAction = () => 'stop';
|
||||
globalThis.showToast = (...args) => M.toasts.push(args);
|
||||
globalThis.t = (key) => key;
|
||||
__SHOW_TOAST_SRC__
|
||||
globalThis.send = () => { M.sends += 1; };
|
||||
globalThis.cancelStream = async () => M.cancelResult;
|
||||
__COMPOSER_PRIMARY_ACTION_SRC__
|
||||
for (const result of [false, true]) {
|
||||
for (const [message, result] of [[__ENGLISH__, false], [__JAPANESE__, false], ['unused', true]]) {
|
||||
M.cancelResult = result;
|
||||
globalThis.t = () => message;
|
||||
const before = M.renders.length;
|
||||
await handleComposerPrimaryAction();
|
||||
M.results.push({ result, toasts: M.toasts.slice(), sends: M.sends });
|
||||
M.results.push({ result, rendered: M.renders.length - before, sends: M.sends });
|
||||
}
|
||||
console.log(JSON.stringify(M));
|
||||
'''.replace("__COMPOSER_PRIMARY_ACTION_SRC__", COMPOSER_PRIMARY_ACTION_SRC)
|
||||
'''.replace("__SHOW_TOAST_SRC__", SHOW_TOAST_SRC)
|
||||
.replace("__COMPOSER_PRIMARY_ACTION_SRC__", COMPOSER_PRIMARY_ACTION_SRC)
|
||||
.replace("__ENGLISH__", json.dumps(english_message))
|
||||
.replace("__JAPANESE__", json.dumps(japanese_message)))
|
||||
completed = subprocess.run(
|
||||
["node", "--input-type=module", "-e", script],
|
||||
cwd=str(REPO), text=True, capture_output=True, timeout=30,
|
||||
cwd=str(REPO), capture_output=True, encoding="utf-8", timeout=30,
|
||||
)
|
||||
assert completed.returncode == 0, (
|
||||
f"node subprocess failed:\n--- stdout ---\n{completed.stdout}\n--- stderr ---\n{completed.stderr}"
|
||||
)
|
||||
result = json.loads(completed.stdout.splitlines()[-1])
|
||||
assert result["renders"] == [
|
||||
{"message": english_message, "className": "toast show error", "duration": 20000, "copy": True},
|
||||
{"message": japanese_message, "className": "toast show error", "duration": 20000, "copy": True},
|
||||
]
|
||||
assert result["results"] == [
|
||||
{"result": False, "toasts": [["cancel_failed"]], "sends": 0},
|
||||
{"result": True, "toasts": [["cancel_failed"]], "sends": 0},
|
||||
{"result": False, "rendered": 1, "sends": 0},
|
||||
{"result": False, "rendered": 1, "sends": 0},
|
||||
{"result": True, "rendered": 0, "sends": 0},
|
||||
]
|
||||
Reference in new issue
Block a user