fix(i18n): localize /goal runtime status strings

This commit is contained in:
Frank Song
2026-05-10 15:21:24 +08:00
parent e4a9c5b7f5
commit 1e1a9481b4
6 changed files with 305 additions and 11 deletions
+123 -4
View File
@@ -4,6 +4,7 @@ from __future__ import annotations
import copy
import logging
import re
import time
from pathlib import Path
from typing import Any, Dict, Optional
@@ -279,6 +280,8 @@ def _payload(
error: str | None = None,
kickoff_prompt: str | None = None,
decision: Dict[str, Any] | None = None,
message_key: str | None = None,
message_args: list[Any] | None = None,
) -> Dict[str, Any]:
body: Dict[str, Any] = {
"ok": bool(ok),
@@ -292,9 +295,98 @@ def _payload(
body["kickoff_prompt"] = kickoff_prompt
if decision is not None:
body["decision"] = decision
if message_key:
body["message_key"] = message_key
if message_args is not None:
body["message_args"] = [a for a in message_args if a is not None]
return body
def _goal_status_payload(state: Any, *, default_message: str | None = None) -> Dict[str, Any]:
"""Build localized-status style payload fields from a goal state."""
if default_message is None:
default_message = "No active goal. Set one with /goal <text>."
if state is None:
return {"message": default_message, "message_key": "goal_status_none"}
status = str(getattr(state, "status", "") or "").strip()
if status in ("cleared",):
return {"message": default_message, "message_key": "goal_status_none"}
turns_used = int(getattr(state, "turns_used", 0) or 0)
max_turns = int(getattr(state, "max_turns", 0) or 0)
goal = str(getattr(state, "goal", "") or "")
if status == "active":
return {
"message": f"⊙ Goal (active, {turns_used}/{max_turns} turns): {goal}",
"message_key": "goal_status_active",
"message_args": [turns_used, max_turns, goal],
}
if status == "paused":
reason = str(getattr(state, "paused_reason", "") or "")
return {
"message": f"⏸ Goal (paused, {turns_used}/{max_turns}{'' + reason if reason else ''}): {goal}",
"message_key": "goal_status_paused",
"message_args": [turns_used, max_turns, reason, goal],
}
if status == "done":
return {
"message": f"✓ Goal done ({turns_used}/{max_turns}): {goal}",
"message_key": "goal_status_done",
"message_args": [turns_used, max_turns, goal],
}
return {
"message": f"Goal ({status}, {turns_used}/{max_turns}): {goal}",
"message_args": [status, turns_used, max_turns, goal],
}
def _extract_goal_turns_from_message(message: str) -> tuple[int, int]:
"""Best-effort extraction for continuation messages like '(1/20)'."""
if not message:
return 0, 0
match = re.search(r"\((\d+)\s*/\s*(\d+)\)", message)
if not match:
return 0, 0
try:
return int(match.group(1)), int(match.group(2))
except Exception:
return 0, 0
def _goal_decision_payload(
decision: Dict[str, Any],
state: Any,
) -> Dict[str, Any]:
"""Attach goal message i18n key/args to an evaluation decision."""
if not isinstance(decision, dict):
return decision
status = str(decision.get("status") or "").strip()
reason = str(decision.get("reason") or "").strip()
turns_used = int(getattr(state, "turns_used", 0) or 0)
max_turns = int(getattr(state, "max_turns", 0) or 0)
if (turns_used, max_turns) == (0, 0):
turns_used, max_turns = _extract_goal_turns_from_message(str(decision.get("message") or ""))
if status == "done":
return {
**decision,
"message_key": "goal_achieved",
"message_args": [reason],
}
if status == "paused":
return {
**decision,
"message_key": "goal_paused_budget_exhausted",
"message_args": [turns_used, max_turns],
}
if decision.get("should_continue"):
return {
**decision,
"message_key": "goal_continuing",
"message_args": [turns_used, max_turns, reason],
}
return decision
def goal_state_snapshot(session_id: str, *, profile_home: str | Path | None = None) -> Any:
"""Return a deep copy of current goal state for rollback before kickoff."""
mgr = _manager(str(session_id or ""), profile_home=profile_home)
@@ -355,24 +447,46 @@ def goal_command_payload(
lower = text.lower()
if not text or lower == "status":
return _payload(action="status", message=mgr.status_line(), state=getattr(mgr, "state", None))
state = getattr(mgr, "state", None)
status_payload = _goal_status_payload(state)
return _payload(action="status", state=state, **status_payload)
if lower == "pause":
state = mgr.pause(reason="user-paused")
if state is None:
return _payload(ok=False, action="pause", error="no_goal", message="No goal set.")
return _payload(action="pause", message=f"⏸ Goal paused: {state.goal}", state=state)
return _payload(
ok=False,
action="pause",
error="no_goal",
message="No goal set.",
message_key="goal_no_goal",
)
return _payload(
action="pause",
message=f"⏸ Goal paused: {state.goal}",
message_key="goal_paused",
message_args=[str(state.goal)],
state=state,
)
if lower == "resume":
state = mgr.resume()
if state is None:
return _payload(ok=False, action="resume", error="no_goal", message="No goal to resume.")
return _payload(
ok=False,
action="resume",
error="no_goal",
message="No goal to resume.",
message_key="goal_no_goal",
)
return _payload(
action="resume",
message=(
f"▶ Goal resumed: {state.goal}\n"
"Send a new message, or type continue, to kick it off."
),
message_key="goal_resumed",
message_args=[str(state.goal)],
state=state,
)
@@ -382,6 +496,7 @@ def goal_command_payload(
return _payload(
action="clear",
message="Goal cleared." if had else "No active goal.",
message_key="goal_cleared" if had else "goal_no_goal",
state=getattr(mgr, "state", None),
)
@@ -408,6 +523,8 @@ def goal_command_payload(
"I'll keep working until the goal is done, you pause/clear it, or the budget is exhausted.\n"
"Controls: /goal status · /goal pause · /goal resume · /goal clear"
),
message_key="goal_set",
message_args=[state.max_turns, state.goal],
state=state,
kickoff_prompt=state.goal,
)
@@ -486,4 +603,6 @@ def evaluate_goal_after_turn(
decision.setdefault("should_continue", False)
decision.setdefault("continuation_prompt", None)
decision.setdefault("message", "")
decision = dict(decision)
decision = _goal_decision_payload(decision, getattr(mgr, "state", None))
return decision
+5
View File
@@ -3320,6 +3320,7 @@ def _run_agent_streaming(
'session_id': session_id,
'state': 'evaluating',
'message': 'Evaluating goal progress…',
'message_key': 'goal_evaluating_progress',
})
_goal_decision = evaluate_goal_after_turn(
session_id,
@@ -3334,6 +3335,8 @@ def _run_agent_streaming(
'session_id': session_id,
'state': 'continuing' if decision.get('should_continue') else 'idle',
'message': _goal_message,
'message_key': decision.get('message_key') or ('goal_continuing' if _goal_message else ''),
'message_args': decision.get('message_args') or [],
'decision': decision,
})
if decision.get('should_continue'):
@@ -3347,6 +3350,8 @@ def _run_agent_streaming(
'continuation_prompt': continuation_prompt,
'text': continuation_prompt,
'message': _goal_message,
'message_key': decision.get('message_key') or 'goal_continuing',
'message_args': decision.get('message_args') or [],
'decision': decision,
})
except Exception as _goal_exc:
+12 -2
View File
@@ -639,7 +639,17 @@ async function cmdGoal(args){
model_provider:S.session.model_provider||null,
profile:S.activeProfile||S.session.profile||'default',
})});
const msg=String((r&&r.message)||'').trim();
const msg = (() => {
const raw = String((r && r.message) || '').trim();
const key = String((r && r.message_key) || '').trim();
const args = Array.isArray(r && r.message_args) ? r.message_args : [];
if (raw.includes('\n')) return raw;
if (key && typeof t === 'function') {
const translated = String(t(key, ...args));
if (translated && translated !== key) return translated;
}
return raw;
})();
if(msg){
S.messages.push({role:'assistant',content:msg,_ts:Date.now()/1000,_goalStatus:true,_transient:true});
renderMessages({preserveScroll:true});
@@ -649,7 +659,7 @@ async function cmdGoal(args){
S.toolCalls=[];
if(typeof clearLiveToolCards==='function')clearLiveToolCards();
appendThinking();setBusy(true);
setComposerStatus('Working toward goal…');
setComposerStatus(t('goal_working_toward'));
S.activeStreamId=r.stream_id;
if(S.session&&S.session.session_id===activeSid){
S.session.active_stream_id=r.stream_id;
+136 -1
View File
@@ -195,6 +195,21 @@ const LOCALES = {
no_active_session: 'No active session',
cmd_queue: 'Queue a message for the next turn',
cmd_goal: 'Set or inspect a persistent goal',
goal_evaluating_progress: 'Evaluating goal progress…',
goal_working_toward: 'Working toward goal…',
goal_continuing_toast: 'Continuing toward goal…',
goal_status_none: 'No active goal. Set one with /goal <text>.',
goal_status_active: (turns, max_turns, goal) => `⊙ Goal (active, ${turns}/${max_turns} turns): ${goal}`,
goal_status_paused: (turns, max_turns, reason, goal) => `⏸ Goal (paused, ${turns}/${max_turns}${reason ? `, ${reason}` : ''}): ${goal}`,
goal_status_done: (turns, max_turns, goal) => `✓ Goal done (${turns}/${max_turns}): ${goal}`,
goal_set: (turns, goal) => `⊙ Goal set (${turns}-turn budget): ${goal}`,
goal_paused: (goal) => `⏸ Goal paused: ${goal}`,
goal_resumed: (goal) => `▶ Goal resumed: ${goal}`,
goal_cleared: 'Goal cleared.',
goal_no_goal: 'No active goal.',
goal_achieved: (reason) => `✓ Goal achieved: ${reason}`,
goal_paused_budget_exhausted: (turns, max_turns) => `⏸ Goal paused — ${turns}/${max_turns} turns used. Use /goal resume to keep going, or /goal clear to stop.`,
goal_continuing: (turns, max_turns, reason) => `↻ Continuing toward goal (${turns}/${max_turns}): ${reason}`,
cmd_interrupt: 'Cancel current turn and send a new message',
cmd_steer: 'Inject a mid-turn correction without interrupting the agent',
cmd_queue_no_msg: 'Usage: /queue <message>',
@@ -1263,6 +1278,21 @@ const LOCALES = {
no_active_session: 'アクティブなセッションがありません',
cmd_queue: '次のターン用にメッセージをキュー',
cmd_goal: '永続ゴールを設定または確認',
goal_evaluating_progress: 'Evaluating goal progress…',
goal_working_toward: 'Working toward goal…',
goal_continuing_toast: 'Continuing toward goal…',
goal_status_none: 'No active goal. Set one with /goal <text>.',
goal_status_active: (turns, max_turns, goal) => `⊙ Goal (active, ${turns}/${max_turns} turns): ${goal}`,
goal_status_paused: (turns, max_turns, reason, goal) => `⏸ Goal (paused, ${turns}/${max_turns}${reason ? `, ${reason}` : ''}): ${goal}`,
goal_status_done: (turns, max_turns, goal) => `✓ Goal done (${turns}/${max_turns}): ${goal}`,
goal_set: (turns, goal) => `⊙ Goal set (${turns}-turn budget): ${goal}`,
goal_paused: (goal) => `⏸ Goal paused: ${goal}`,
goal_resumed: (goal) => `▶ Goal resumed: ${goal}`,
goal_cleared: 'Goal cleared.',
goal_no_goal: 'No active goal.',
goal_achieved: (reason) => `✓ Goal achieved: ${reason}`,
goal_paused_budget_exhausted: (turns, max_turns) => `⏸ Goal paused — ${turns}/${max_turns} turns used. Use /goal resume to keep going, or /goal clear to stop.`,
goal_continuing: (turns, max_turns, reason) => `↻ Continuing toward goal (${turns}/${max_turns}): ${reason}`,
cmd_interrupt: '現在のターンをキャンセルして新規メッセージを送信',
cmd_steer: 'エージェントを中断せずにターン中に修正を注入',
cmd_queue_no_msg: '使い方: /queue <メッセージ>',
@@ -2292,6 +2322,21 @@ const LOCALES = {
no_active_session: 'Нет активной сессии',
cmd_queue: 'Поставить сообщение в очередь на следующий оборот',
cmd_goal: 'Задать или проверить постоянную цель',
goal_evaluating_progress: 'Evaluating goal progress…',
goal_working_toward: 'Working toward goal…',
goal_continuing_toast: 'Continuing toward goal…',
goal_status_none: 'No active goal. Set one with /goal <text>.',
goal_status_active: (turns, max_turns, goal) => `⊙ Goal (active, ${turns}/${max_turns} turns): ${goal}`,
goal_status_paused: (turns, max_turns, reason, goal) => `⏸ Goal (paused, ${turns}/${max_turns}${reason ? `, ${reason}` : ''}): ${goal}`,
goal_status_done: (turns, max_turns, goal) => `✓ Goal done (${turns}/${max_turns}): ${goal}`,
goal_set: (turns, goal) => `⊙ Goal set (${turns}-turn budget): ${goal}`,
goal_paused: (goal) => `⏸ Goal paused: ${goal}`,
goal_resumed: (goal) => `▶ Goal resumed: ${goal}`,
goal_cleared: 'Goal cleared.',
goal_no_goal: 'No active goal.',
goal_achieved: (reason) => `✓ Goal achieved: ${reason}`,
goal_paused_budget_exhausted: (turns, max_turns) => `⏸ Goal paused — ${turns}/${max_turns} turns used. Use /goal resume to keep going, or /goal clear to stop.`,
goal_continuing: (turns, max_turns, reason) => `↻ Continuing toward goal (${turns}/${max_turns}): ${reason}`,
cmd_interrupt: 'Прервать текущий оборот и отправить новое сообщение',
cmd_steer: 'Направить агента исправлением (переходит к прерыванию)',
cmd_queue_no_msg: 'Использование: /queue <сообщение>',
@@ -3326,6 +3371,21 @@ const LOCALES = {
no_active_session: 'No hay ninguna sesión activa',
cmd_queue: 'Poner mensaje en cola para el siguiente turno',
cmd_goal: 'Definir o consultar un objetivo persistente',
goal_evaluating_progress: 'Evaluating goal progress…',
goal_working_toward: 'Working toward goal…',
goal_continuing_toast: 'Continuing toward goal…',
goal_status_none: 'No active goal. Set one with /goal <text>.',
goal_status_active: (turns, max_turns, goal) => `⊙ Goal (active, ${turns}/${max_turns} turns): ${goal}`,
goal_status_paused: (turns, max_turns, reason, goal) => `⏸ Goal (paused, ${turns}/${max_turns}${reason ? `, ${reason}` : ''}): ${goal}`,
goal_status_done: (turns, max_turns, goal) => `✓ Goal done (${turns}/${max_turns}): ${goal}`,
goal_set: (turns, goal) => `⊙ Goal set (${turns}-turn budget): ${goal}`,
goal_paused: (goal) => `⏸ Goal paused: ${goal}`,
goal_resumed: (goal) => `▶ Goal resumed: ${goal}`,
goal_cleared: 'Goal cleared.',
goal_no_goal: 'No active goal.',
goal_achieved: (reason) => `✓ Goal achieved: ${reason}`,
goal_paused_budget_exhausted: (turns, max_turns) => `⏸ Goal paused — ${turns}/${max_turns} turns used. Use /goal resume to keep going, or /goal clear to stop.`,
goal_continuing: (turns, max_turns, reason) => `↻ Continuing toward goal (${turns}/${max_turns}): ${reason}`,
cmd_interrupt: 'Cancelar turno actual y enviar nuevo mensaje',
cmd_steer: 'Inyectar una corrección a mitad del turno sin interrumpir al agente',
cmd_queue_no_msg: 'Uso: /queue <mensaje>',
@@ -4309,6 +4369,21 @@ const LOCALES = {
model_scope_toast: 'Gilt für diesen Chat ab Ihrer nächsten Nachricht.',
cmd_queue: 'Nachricht f\u00fcr den n\u00e4chsten Durchgang einreihen',
cmd_goal: 'Ein dauerhaftes Ziel setzen oder prüfen',
goal_evaluating_progress: 'Evaluating goal progress…',
goal_working_toward: 'Working toward goal…',
goal_continuing_toast: 'Continuing toward goal…',
goal_status_none: 'No active goal. Set one with /goal <text>.',
goal_status_active: (turns, max_turns, goal) => `⊙ Goal (active, ${turns}/${max_turns} turns): ${goal}`,
goal_status_paused: (turns, max_turns, reason, goal) => `⏸ Goal (paused, ${turns}/${max_turns}${reason ? `, ${reason}` : ''}): ${goal}`,
goal_status_done: (turns, max_turns, goal) => `✓ Goal done (${turns}/${max_turns}): ${goal}`,
goal_set: (turns, goal) => `⊙ Goal set (${turns}-turn budget): ${goal}`,
goal_paused: (goal) => `⏸ Goal paused: ${goal}`,
goal_resumed: (goal) => `▶ Goal resumed: ${goal}`,
goal_cleared: 'Goal cleared.',
goal_no_goal: 'No active goal.',
goal_achieved: (reason) => `✓ Goal achieved: ${reason}`,
goal_paused_budget_exhausted: (turns, max_turns) => `⏸ Goal paused — ${turns}/${max_turns} turns used. Use /goal resume to keep going, or /goal clear to stop.`,
goal_continuing: (turns, max_turns, reason) => `↻ Continuing toward goal (${turns}/${max_turns}): ${reason}`,
cmd_interrupt: 'Aktuellen Durchgang abbrechen und neue Nachricht senden',
cmd_steer: 'Korrektursignal einf\u00fcgen ohne Unterbrechung',
cmd_queue_no_msg: 'Verwendung: /queue <Nachricht>',
@@ -5027,7 +5102,7 @@ const LOCALES = {
profile_gateway_stopped: 'Gateway gestoppt',
profile_active: 'Aktiv',
profile_no_configuration: 'Keine Konfiguration',
profile_skill_count: '{count} Fähigkeiten',
profile_skill_count: (count) => `${count} Fähigkeit${count === 1 ? '' : 'en'}`,
profile_use: 'Verwenden',
profile_switch_title: 'Profil wechseln',
profile_delete_title: 'Profil löschen',
@@ -5329,6 +5404,21 @@ const LOCALES = {
no_active_session: '\u5f53\u524d\u6ca1\u6709\u6d3b\u52a8\u4f1a\u8bdd',
cmd_queue: '\u5c06\u6d88\u606f\u52a0\u5165\u4e0b\u4e00\u8f6e\u7684\u961f\u5217',
cmd_goal: '设置或查看持久目标',
goal_evaluating_progress: 'Evaluating goal progress…',
goal_working_toward: 'Working toward goal…',
goal_continuing_toast: 'Continuing toward goal…',
goal_status_none: 'No active goal. Set one with /goal <text>.',
goal_status_active: (turns, max_turns, goal) => `⊙ Goal (active, ${turns}/${max_turns} turns): ${goal}`,
goal_status_paused: (turns, max_turns, reason, goal) => `⏸ Goal (paused, ${turns}/${max_turns}${reason ? `, ${reason}` : ''}): ${goal}`,
goal_status_done: (turns, max_turns, goal) => `✓ Goal done (${turns}/${max_turns}): ${goal}`,
goal_set: (turns, goal) => `⊙ Goal set (${turns}-turn budget): ${goal}`,
goal_paused: (goal) => `⏸ Goal paused: ${goal}`,
goal_resumed: (goal) => `▶ Goal resumed: ${goal}`,
goal_cleared: 'Goal cleared.',
goal_no_goal: 'No active goal.',
goal_achieved: (reason) => `✓ Goal achieved: ${reason}`,
goal_paused_budget_exhausted: (turns, max_turns) => `⏸ Goal paused — ${turns}/${max_turns} turns used. Use /goal resume to keep going, or /goal clear to stop.`,
goal_continuing: (turns, max_turns, reason) => `↻ Continuing toward goal (${turns}/${max_turns}): ${reason}`,
cmd_interrupt: '\u53d6\u6d88\u5f53\u524d\u56de\u5408\u5e76\u53d1\u9001\u65b0\u6d88\u606f',
cmd_steer: '\u7528\u7ea0\u6b63\u4fe1\u606f\u5f15\u5bfc\u4ee3\u7406\uff08\u56de\u9000\u4e3a\u4e2d\u65ad\uff09',
cmd_queue_no_msg: '\u7528\u6cd5\uff1a/queue <\u6d88\u606f>',
@@ -6846,6 +6936,21 @@ const LOCALES = {
no_active_session: '\u7121\u6d3b\u8e8d\u6703\u8a71',
cmd_queue: '\u5c07\u8a0a\u606f\u52a0\u5165\u4e0b\u4e00\u8f2a\u7684\u4f47\u5217',
cmd_goal: '設定或查看持久目標',
goal_evaluating_progress: 'Evaluating goal progress…',
goal_working_toward: 'Working toward goal…',
goal_continuing_toast: 'Continuing toward goal…',
goal_status_none: 'No active goal. Set one with /goal <text>.',
goal_status_active: (turns, max_turns, goal) => `⊙ Goal (active, ${turns}/${max_turns} turns): ${goal}`,
goal_status_paused: (turns, max_turns, reason, goal) => `⏸ Goal (paused, ${turns}/${max_turns}${reason ? `, ${reason}` : ''}): ${goal}`,
goal_status_done: (turns, max_turns, goal) => `✓ Goal done (${turns}/${max_turns}): ${goal}`,
goal_set: (turns, goal) => `⊙ Goal set (${turns}-turn budget): ${goal}`,
goal_paused: (goal) => `⏸ Goal paused: ${goal}`,
goal_resumed: (goal) => `▶ Goal resumed: ${goal}`,
goal_cleared: 'Goal cleared.',
goal_no_goal: 'No active goal.',
goal_achieved: (reason) => `✓ Goal achieved: ${reason}`,
goal_paused_budget_exhausted: (turns, max_turns) => `⏸ Goal paused — ${turns}/${max_turns} turns used. Use /goal resume to keep going, or /goal clear to stop.`,
goal_continuing: (turns, max_turns, reason) => `↻ Continuing toward goal (${turns}/${max_turns}): ${reason}`,
cmd_interrupt: '\u53d6\u6d88\u7576\u524d\u56de\u5408\u4e26\u767c\u9001\u65b0\u8a0a\u606f',
cmd_steer: '\u5728\u56de\u5408\u9032\u884c\u4e2d\u6ce8\u5165\u7d3a\u6b63\uff0c\u4e0d\u4e2d\u65b7\u4ee3\u7406',
cmd_queue_no_msg: '\u7528\u6cd5\uff1a/queue <\u8a0a\u606f>',
@@ -7371,6 +7476,21 @@ const LOCALES = {
no_active_session: 'Nenhuma sessão ativa',
cmd_queue: 'Enfileirar mensagem para o próximo turno',
cmd_goal: 'Definir ou consultar uma meta persistente',
goal_evaluating_progress: 'Evaluating goal progress…',
goal_working_toward: 'Working toward goal…',
goal_continuing_toast: 'Continuing toward goal…',
goal_status_none: 'No active goal. Set one with /goal <text>.',
goal_status_active: (turns, max_turns, goal) => `⊙ Goal (active, ${turns}/${max_turns} turns): ${goal}`,
goal_status_paused: (turns, max_turns, reason, goal) => `⏸ Goal (paused, ${turns}/${max_turns}${reason ? `, ${reason}` : ''}): ${goal}`,
goal_status_done: (turns, max_turns, goal) => `✓ Goal done (${turns}/${max_turns}): ${goal}`,
goal_set: (turns, goal) => `⊙ Goal set (${turns}-turn budget): ${goal}`,
goal_paused: (goal) => `⏸ Goal paused: ${goal}`,
goal_resumed: (goal) => `▶ Goal resumed: ${goal}`,
goal_cleared: 'Goal cleared.',
goal_no_goal: 'No active goal.',
goal_achieved: (reason) => `✓ Goal achieved: ${reason}`,
goal_paused_budget_exhausted: (turns, max_turns) => `⏸ Goal paused — ${turns}/${max_turns} turns used. Use /goal resume to keep going, or /goal clear to stop.`,
goal_continuing: (turns, max_turns, reason) => `↻ Continuing toward goal (${turns}/${max_turns}): ${reason}`,
cmd_interrupt: 'Cancelar turno atual e enviar nova mensagem',
cmd_steer: 'Injetar correção no meio do turno sem interromper',
cmd_queue_no_msg: 'Uso: /queue <mensagem>',
@@ -8315,6 +8435,21 @@ const LOCALES = {
no_active_session: '활성 세션 없음',
cmd_queue: 'Queue a message for the next turn',
cmd_goal: '지속 목표를 설정하거나 확인',
goal_evaluating_progress: 'Evaluating goal progress…',
goal_working_toward: 'Working toward goal…',
goal_continuing_toast: 'Continuing toward goal…',
goal_status_none: 'No active goal. Set one with /goal <text>.',
goal_status_active: (turns, max_turns, goal) => `⊙ Goal (active, ${turns}/${max_turns} turns): ${goal}`,
goal_status_paused: (turns, max_turns, reason, goal) => `⏸ Goal (paused, ${turns}/${max_turns}${reason ? `, ${reason}` : ''}): ${goal}`,
goal_status_done: (turns, max_turns, goal) => `✓ Goal done (${turns}/${max_turns}): ${goal}`,
goal_set: (turns, goal) => `⊙ Goal set (${turns}-turn budget): ${goal}`,
goal_paused: (goal) => `⏸ Goal paused: ${goal}`,
goal_resumed: (goal) => `▶ Goal resumed: ${goal}`,
goal_cleared: 'Goal cleared.',
goal_no_goal: 'No active goal.',
goal_achieved: (reason) => `✓ Goal achieved: ${reason}`,
goal_paused_budget_exhausted: (turns, max_turns) => `⏸ Goal paused — ${turns}/${max_turns} turns used. Use /goal resume to keep going, or /goal clear to stop.`,
goal_continuing: (turns, max_turns, reason) => `↻ Continuing toward goal (${turns}/${max_turns}): ${reason}`,
cmd_interrupt: 'Cancel current turn and send a new message',
cmd_steer: 'Inject a mid-turn correction without interrupting the agent',
cmd_queue_no_msg: 'Usage: /queue <message>',
+18 -3
View File
@@ -896,17 +896,30 @@ function attachLiveStream(activeSid, streamId, uploaded=[], options={}){
}catch(_){}
});
function _resolveGoalMessage(d){
const key=String(d && d.message_key ? d.message_key : '').trim();
const args=Array.isArray(d && d.message_args) ? d.message_args : [];
const raw=String(d&&d.message||'').trim();
if(key && typeof t==='function'){
try{
const translated=String(t(key,...args));
if(translated && translated!==key)return translated;
}catch(_){}
}
return raw;
}
source.addEventListener('goal',e=>{
try{
const d=JSON.parse(e.data||'{}');
if((d.session_id||activeSid)!==activeSid) return;
const goalState=String(d.state||'').trim();
const goalEvaluatingMessage='Evaluating goal progress';
const goalEvaluatingMessage=t('goal_evaluating_progress');
if(goalState==='evaluating'){
setComposerStatus(goalEvaluatingMessage);
return;
}
const msg=String(d.message||'').trim();
const msg=_resolveGoalMessage(d);
if(!msg)return;
_latestGoalStatus={message:msg,decision:d.decision||null,state:goalState||null};
setComposerStatus(msg);
@@ -927,7 +940,9 @@ function attachLiveStream(activeSid, streamId, uploaded=[], options={}){
model_provider:S.session&&S.session.model_provider||null,
profile:S.activeProfile||'default',
};
showToast('Continuing toward goal…',2200);
const toast=t('goal_continuing_toast');
const cmsg=_resolveGoalMessage(d);
showToast((toast&&cmsg&&cmsg!==toast)?cmsg.split('\n')[0]:toast,2200);
}catch(_){}
});
+11 -1
View File
@@ -68,10 +68,18 @@ def test_goal_command_payload_matches_gateway_controls(monkeypatch):
set_goal = webui_goals.goal_command_payload("sid-123", "ship the feature")
assert status["message"] == "No active goal. Set one with /goal <text>."
assert status["message_key"] == "goal_status_none"
assert pause["message"] == "⏸ Goal paused: ship the feature"
assert pause["message_key"] == "goal_paused"
assert pause["message_args"] == ["ship the feature"]
assert resume["message"].startswith("▶ Goal resumed: ship the feature")
assert resume["message_key"] == "goal_resumed"
assert resume["message_args"] == ["ship the feature"]
assert clear["message"] == "Goal cleared."
assert clear["message_key"] == "goal_cleared"
assert set_goal["action"] == "set"
assert set_goal["message_key"] == "goal_set"
assert set_goal["message_args"] == [20, "ship the feature"]
assert set_goal["kickoff_prompt"] == "ship the feature"
assert "⊙ Goal set (20-turn budget): ship the feature" in set_goal["message"]
assert ("set", "ship the feature") in calls
@@ -145,6 +153,8 @@ def test_goal_continuation_decision_emits_status_and_normal_user_prompt(monkeypa
decision = webui_goals.evaluate_goal_after_turn("sid-123", "not done yet", user_initiated=False)
assert decision["message_key"] == "goal_continuing"
assert decision["message_args"] == [1, 20, "one step remains"]
assert decision["message"].startswith("↻ Continuing toward goal")
assert decision["should_continue"] is True
assert decision["continuation_prompt"].startswith("[Continuing toward your standing goal]")
@@ -266,7 +276,7 @@ def test_frontend_has_goal_slash_command_and_status_event_handler():
def test_frontend_goal_evaluating_state_uses_calm_composer_indicator():
assert "const goalState=String(d.state||'').trim();" in MESSAGES_JS
assert "const goalEvaluatingMessage='Evaluating goal progress…';" in MESSAGES_JS
assert "t('goal_evaluating_progress')" in MESSAGES_JS
assert "if(goalState==='evaluating')" in MESSAGES_JS
assert "setComposerStatus(goalEvaluatingMessage);" in MESSAGES_JS
assert "return;" in MESSAGES_JS