fix(#6008): keep stop failure toasts actionable across locales
This commit is contained in:
parent
dc4078426a
commit
6d9e4ac4c5
5 changed files with 84 additions and 24 deletions
|
|
@ -1215,7 +1215,7 @@ async function cmdStop(){
|
||||||
if(!S.activeStreamId){showToast(t('no_active_task'));return;}
|
if(!S.activeStreamId){showToast(t('no_active_task'));return;}
|
||||||
if(typeof cancelStream==='function'){
|
if(typeof cancelStream==='function'){
|
||||||
if(await cancelStream('slash-stop')) showToast(t('stream_stopped'));
|
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'));
|
else showToast(t('cancel_unavailable'));
|
||||||
}
|
}
|
||||||
|
|
@ -1324,7 +1324,7 @@ async function cmdInterrupt(args){
|
||||||
// Cancel the active stream; setBusy(false) will drain the queue
|
// Cancel the active stream; setBusy(false) will drain the queue
|
||||||
if(typeof cancelStream==='function'){
|
if(typeof cancelStream==='function'){
|
||||||
if(await cancelStream('slash-interrupt')) showToast(t('cmd_interrupt_confirm'),2000);
|
if(await cancelStream('slash-interrupt')) showToast(t('cmd_interrupt_confirm'),2000);
|
||||||
else showToast(t('cancel_failed'));
|
else showToast(t('cancel_failed'),null,'error');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1396,7 +1396,7 @@ async function send(){
|
||||||
S.pendingFiles=[];renderTray();
|
S.pendingFiles=[];renderTray();
|
||||||
if(S.activeStreamId&&typeof cancelStream==='function'){
|
if(S.activeStreamId&&typeof cancelStream==='function'){
|
||||||
if(await cancelStream('busy-interrupt')) showToast(t('busy_interrupt_confirm'),2000);
|
if(await cancelStream('busy-interrupt')) showToast(t('busy_interrupt_confirm'),2000);
|
||||||
else showToast(t('cancel_failed'));
|
else showToast(t('cancel_failed'),null,'error');
|
||||||
} else {
|
} else {
|
||||||
showToast(`Queued: "${text.slice(0,40)}${text.length>40?'…':''}"`,2000);
|
showToast(`Queued: "${text.slice(0,40)}${text.length>40?'…':''}"`,2000);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -4907,7 +4907,7 @@ function _openSessionActionMenu(session, anchorEl){
|
||||||
async()=>{
|
async()=>{
|
||||||
closeSessionActionMenu();
|
closeSessionActionMenu();
|
||||||
if(await cancelSessionStream(session)) showToast(t('stream_stopped'));
|
if(await cancelSessionStream(session)) showToast(t('stream_stopped'));
|
||||||
else showToast(t('cancel_failed'));
|
else showToast(t('cancel_failed'),null,'error');
|
||||||
}
|
}
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -7901,7 +7901,7 @@ async function handleComposerPrimaryAction(){
|
||||||
const action=typeof getComposerPrimaryAction==='function'?getComposerPrimaryAction():'send';
|
const action=typeof getComposerPrimaryAction==='function'?getComposerPrimaryAction():'send';
|
||||||
if(action==='disabled') return;
|
if(action==='disabled') return;
|
||||||
if(action==='stop'){
|
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;
|
return;
|
||||||
}
|
}
|
||||||
await send();
|
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")
|
COMMANDS_JS = (REPO / "static" / "commands.js").read_text(encoding="utf-8")
|
||||||
MESSAGES_JS = (REPO / "static" / "messages.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")
|
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:
|
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"
|
assert m, f"{name} not found in static/boot.js"
|
||||||
brace_pos = src.index("{", m.end())
|
brace_pos = src.index("{", m.end())
|
||||||
depth = 1
|
depth = 1
|
||||||
|
|
@ -33,6 +34,11 @@ def _extract_function(src: str, name: str) -> str:
|
||||||
|
|
||||||
CANCEL_SESSION_STREAM_SRC = _extract_function(BOOT_JS, "cancelSessionStream")
|
CANCEL_SESSION_STREAM_SRC = _extract_function(BOOT_JS, "cancelSessionStream")
|
||||||
COMPOSER_PRIMARY_ACTION_SRC = _extract_function(UI_JS, "handleComposerPrimaryAction")
|
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():
|
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_commands = "".join(COMMANDS_JS.split())
|
||||||
compact_messages = "".join(MESSAGES_JS.split())
|
compact_messages = "".join(MESSAGES_JS.split())
|
||||||
compact_sessions = "".join(SESSIONS_JS.split())
|
compact_sessions = "".join(SESSIONS_JS.split())
|
||||||
assert "if(awaitcancelStream('slash-stop'))showToast(t('stream_stopped'))" in compact_commands
|
assert (
|
||||||
assert "elseshowToast(t('cancel_failed'))" in compact_commands
|
"if(awaitcancelStream('slash-stop'))showToast(t('stream_stopped'));"
|
||||||
assert "if(awaitcancelStream('slash-interrupt'))showToast(t('cmd_interrupt_confirm'),2000)" in compact_commands
|
"elseshowToast(t('cancel_failed'),null,'error');"
|
||||||
assert "if(awaitcancelStream('busy-interrupt'))showToast(t('busy_interrupt_confirm'),2000)" in compact_messages
|
) in compact_commands
|
||||||
assert "if(awaitcancelSessionStream(session))showToast(t('stream_stopped'))" in compact_sessions
|
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'''
|
_NODE_SCRIPT = r'''
|
||||||
|
|
@ -124,8 +145,8 @@ def test_failed_sidebar_stop_keeps_local_state():
|
||||||
completed = subprocess.run(
|
completed = subprocess.run(
|
||||||
["node", "--input-type=module", "-e", script],
|
["node", "--input-type=module", "-e", script],
|
||||||
cwd=str(REPO),
|
cwd=str(REPO),
|
||||||
text=True,
|
|
||||||
capture_output=True,
|
capture_output=True,
|
||||||
|
encoding="utf-8",
|
||||||
timeout=30,
|
timeout=30,
|
||||||
)
|
)
|
||||||
assert completed.returncode == 0, (
|
assert completed.returncode == 0, (
|
||||||
|
|
@ -146,33 +167,72 @@ def test_failed_sidebar_stop_keeps_local_state():
|
||||||
assert result["clarifyHides"] == 0
|
assert result["clarifyHides"] == 0
|
||||||
|
|
||||||
|
|
||||||
def test_primary_composer_stop_surfaces_cancel_failure_and_preserves_success():
|
def test_primary_composer_stop_renders_localized_error_and_preserves_success():
|
||||||
script = r'''
|
cancel_key = "cancel_" + "failed"
|
||||||
const M = { toasts: [], sends: 0, results: [] };
|
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.window = {};
|
||||||
globalThis.S = {};
|
globalThis.S = {};
|
||||||
globalThis.getComposerPrimaryAction = () => 'stop';
|
globalThis.getComposerPrimaryAction = () => 'stop';
|
||||||
globalThis.showToast = (...args) => M.toasts.push(args);
|
__SHOW_TOAST_SRC__
|
||||||
globalThis.t = (key) => key;
|
|
||||||
globalThis.send = () => { M.sends += 1; };
|
globalThis.send = () => { M.sends += 1; };
|
||||||
globalThis.cancelStream = async () => M.cancelResult;
|
globalThis.cancelStream = async () => M.cancelResult;
|
||||||
__COMPOSER_PRIMARY_ACTION_SRC__
|
__COMPOSER_PRIMARY_ACTION_SRC__
|
||||||
for (const result of [false, true]) {
|
for (const [message, result] of [[__ENGLISH__, false], [__JAPANESE__, false], ['unused', true]]) {
|
||||||
M.cancelResult = result;
|
M.cancelResult = result;
|
||||||
|
globalThis.t = () => message;
|
||||||
|
const before = M.renders.length;
|
||||||
await handleComposerPrimaryAction();
|
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));
|
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(
|
completed = subprocess.run(
|
||||||
["node", "--input-type=module", "-e", script],
|
["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, (
|
assert completed.returncode == 0, (
|
||||||
f"node subprocess failed:\n--- stdout ---\n{completed.stdout}\n--- stderr ---\n{completed.stderr}"
|
f"node subprocess failed:\n--- stdout ---\n{completed.stdout}\n--- stderr ---\n{completed.stderr}"
|
||||||
)
|
)
|
||||||
result = json.loads(completed.stdout.splitlines()[-1])
|
result = json.loads(completed.stdout.splitlines()[-1])
|
||||||
assert result["results"] == [
|
assert result["renders"] == [
|
||||||
{"result": False, "toasts": [["cancel_failed"]], "sends": 0},
|
{"message": english_message, "className": "toast show error", "duration": 20000, "copy": True},
|
||||||
{"result": True, "toasts": [["cancel_failed"]], "sends": 0},
|
{"message": japanese_message, "className": "toast show error", "duration": 20000, "copy": True},
|
||||||
|
]
|
||||||
|
assert result["results"] == [
|
||||||
|
{"result": False, "rendered": 1, "sends": 0},
|
||||||
|
{"result": False, "rendered": 1, "sends": 0},
|
||||||
|
{"result": True, "rendered": 0, "sends": 0},
|
||||||
]
|
]
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue