mirror of
https://github.com/nesquena/hermes-webui.git
synced 2026-05-26 11:40:26 +00:00
Stage 298: PR #1667 — feat: add WebUI status command card by @Michaelyklam
This commit is contained in:
Binary file not shown.
|
After Width: | Height: | Size: 58 KiB |
+60
-28
@@ -819,35 +819,67 @@ async function cmdBackground(args){
|
||||
if(typeof startBackgroundPolling==='function') startBackgroundPolling(activeSid,r.task_id,prompt);
|
||||
}catch(e){showToast(t('bg_failed')+e.message);}
|
||||
}
|
||||
async function cmdStatus(){
|
||||
function _formatStatusTimestamp(value){
|
||||
if(value===undefined||value===null||value==='') return t('status_unknown');
|
||||
let date;
|
||||
if(typeof value==='number') date=new Date(value < 1000000000000 ? value*1000 : value);
|
||||
else date=new Date(value);
|
||||
if(Number.isNaN(date.getTime())) return t('status_unknown');
|
||||
return date.toLocaleString();
|
||||
}
|
||||
function _formatStatusTokens(s){
|
||||
const lastUsage=(typeof S!=='undefined'&&(S.lastUsage||s.last_usage))||{};
|
||||
const input=Number(s.input_tokens??lastUsage.input_tokens??0)||0;
|
||||
const output=Number(s.output_tokens??lastUsage.output_tokens??0)||0;
|
||||
const total=Number(s.total_tokens??lastUsage.total_tokens??(input+output))||0;
|
||||
const cost=Number(s.estimated_cost??lastUsage.estimated_cost??0)||0;
|
||||
if(!total&&!cost) return t('status_no_tokens');
|
||||
const fmtNum=n=>Number(n||0).toLocaleString();
|
||||
return `${fmtNum(input)} in / ${fmtNum(output)} out${cost?` (~$${cost.toFixed(4)})`:''}`;
|
||||
}
|
||||
function _statusProviderForSession(s){
|
||||
if(s.model_provider) return String(s.model_provider);
|
||||
if(window._activeProvider) return String(window._activeProvider);
|
||||
const model=String(s.model||'');
|
||||
return model.includes('/') ? model.split('/')[0] : '';
|
||||
}
|
||||
function _statusCardFromSession(s){
|
||||
const provider=_statusProviderForSession(s);
|
||||
const model=s.model||(($('modelSelect')&&$('modelSelect').value)||t('usage_default_model'));
|
||||
const running=!!(s.active_stream_id||S.activeStreamId||S.busy);
|
||||
const profile=s.profile||S.activeProfile||'default';
|
||||
const workspace=s.workspace||S.currentDir||t('status_unknown');
|
||||
const rows=[
|
||||
{label:t('status_session_id'), value:s.session_id||t('status_unknown')},
|
||||
{label:t('status_title'), value:s.title||t('untitled')},
|
||||
{label:t('status_model'), value:model},
|
||||
{label:t('status_provider'), value:provider||t('status_unknown')},
|
||||
{label:t('status_profile'), value:profile},
|
||||
{label:t('status_workspace'), value:workspace},
|
||||
{label:t('status_personality'), value:s.personality||t('usage_personality_none')},
|
||||
{label:t('status_started'), value:_formatStatusTimestamp(s.created_at)},
|
||||
{label:t('status_updated'), value:_formatStatusTimestamp(s.updated_at||s.last_message_at)},
|
||||
{label:t('status_tokens'), value:_formatStatusTokens(s)},
|
||||
{label:t('status_messages'), value:String(s.message_count??(S.messages||[]).filter(m=>m&&m.role&&m.role!=='tool').length)},
|
||||
{label:t('status_agent_running'), value:running?t('status_yes'):t('status_no')},
|
||||
];
|
||||
return {
|
||||
title:t('status_heading'),
|
||||
subtitle:t('status_ephemeral'),
|
||||
sessionId:s.session_id||'',
|
||||
rows,
|
||||
};
|
||||
}
|
||||
function cmdStatus(){
|
||||
if(!S.session){showToast(t('no_active_session'));return;}
|
||||
try{
|
||||
const r=await api('/api/session/status?session_id='+encodeURIComponent(S.session.session_id));
|
||||
if(r&&r.error){showToast(r.error);return;}
|
||||
// Build status card lines matching CLI /status output
|
||||
const provider=window._activeProvider||'';
|
||||
const profile=r.profile||S.activeProfile||'default';
|
||||
const started=r.created_at?new Date(r.created_at).toLocaleString():t('status_unknown');
|
||||
const fmtNum=n=>typeof n==='number'?n.toLocaleString():'0';
|
||||
const tokens=r.total_tokens?`${fmtNum(r.input_tokens)} in / ${fmtNum(r.output_tokens)} out`:t('status_no_tokens');
|
||||
const cost=r.estimated_cost?` (~$${Number(r.estimated_cost).toFixed(4)})`:'';
|
||||
const lines=[
|
||||
`**${t('status_heading')}**`,'',
|
||||
`\`${r.session_id}\``,'',
|
||||
`**${t('status_title')}:** ${r.title||t('untitled')}`,
|
||||
`**${t('status_model')}:** ${r.model||t('usage_default_model')}${provider?' ('+provider+')':''}`,
|
||||
`**${t('status_profile')}:** ${profile}`,
|
||||
`**${t('status_hermes_home')}:** ${r.hermes_home||t('status_unknown')}`,
|
||||
`**${t('status_workspace')}:** ${r.workspace}`,
|
||||
`**${t('status_personality')}:** ${r.personality||t('usage_personality_none')}`,
|
||||
`**${t('status_started')}:** ${started}`,
|
||||
`**${t('status_tokens')}:** ${tokens}${cost}`,
|
||||
`**${t('status_messages')}:** ${r.message_count}`,
|
||||
`**${t('status_agent_running')}:** ${r.agent_running?t('status_yes'):t('status_no')}`,
|
||||
];
|
||||
S.messages.push({role:'assistant',content:lines.join('\n')});
|
||||
renderMessages();
|
||||
}catch(e){showToast(t('status_load_failed')+e.message);}
|
||||
S.messages.push({
|
||||
role:'assistant',
|
||||
content:'',
|
||||
_ephemeral:true,
|
||||
_statusCard:_statusCardFromSession(S.session),
|
||||
_ts:Date.now()/1000,
|
||||
});
|
||||
renderMessages();
|
||||
}
|
||||
function cmdReasoning(args){
|
||||
const arg=(args||'').trim().toLowerCase();
|
||||
|
||||
@@ -225,6 +225,7 @@ const LOCALES = {
|
||||
status_session_id:'Session ID',
|
||||
status_title:'Title',
|
||||
status_model:'Model',
|
||||
status_provider:'Provider',
|
||||
status_workspace:'Workspace',
|
||||
status_personality:'Personality',
|
||||
status_messages:'Messages',
|
||||
@@ -232,7 +233,9 @@ const LOCALES = {
|
||||
status_profile: 'Profile',
|
||||
status_hermes_home: 'Hermes home',
|
||||
status_started: 'Started',
|
||||
status_updated: 'Updated',
|
||||
status_tokens: 'Tokens',
|
||||
status_ephemeral: 'Ephemeral snapshot — not saved to transcript history.',
|
||||
status_no_tokens: 'No tokens used',
|
||||
status_unknown: 'Unknown',
|
||||
status_yes:'Yes',
|
||||
@@ -1185,6 +1188,7 @@ const LOCALES = {
|
||||
status_session_id:'セッションID',
|
||||
status_title:'タイトル',
|
||||
status_model:'モデル',
|
||||
status_provider:'プロバイダー',
|
||||
status_workspace:'ワークスペース',
|
||||
status_personality:'パーソナリティ',
|
||||
status_messages:'メッセージ',
|
||||
@@ -1193,6 +1197,8 @@ const LOCALES = {
|
||||
status_hermes_home: 'Hermes ホーム',
|
||||
status_started: '開始',
|
||||
status_tokens: 'トークン',
|
||||
status_updated: '更新',
|
||||
status_ephemeral: '一時的なスナップショット — 履歴には保存されません。',
|
||||
status_no_tokens: 'トークン未使用',
|
||||
status_unknown: '不明',
|
||||
status_yes:'はい',
|
||||
@@ -2714,6 +2720,8 @@ const LOCALES = {
|
||||
settings_tab_conversation: 'Conversation',
|
||||
settings_tab_preferences: 'Preferences',
|
||||
settings_tab_system: 'System',
|
||||
status_updated: 'Updated',
|
||||
status_ephemeral: 'Ephemeral snapshot — not saved to transcript history.',
|
||||
status_no_tokens: 'No token data',
|
||||
status_profile: 'Profile',
|
||||
status_hermes_home: 'Hermes home',
|
||||
@@ -3598,6 +3606,8 @@ const LOCALES = {
|
||||
settings_tab_conversation: 'Conversation',
|
||||
settings_tab_preferences: 'Preferences',
|
||||
settings_tab_system: 'System',
|
||||
status_updated: 'Updated',
|
||||
status_ephemeral: 'Ephemeral snapshot — not saved to transcript history.',
|
||||
status_no_tokens: 'No token data',
|
||||
status_profile: 'Profile',
|
||||
status_hermes_home: 'Hermes home',
|
||||
@@ -4229,6 +4239,8 @@ const LOCALES = {
|
||||
settings_tab_conversation: 'Conversation',
|
||||
settings_tab_preferences: 'Preferences',
|
||||
settings_tab_system: 'System',
|
||||
status_updated: 'Updated',
|
||||
status_ephemeral: 'Ephemeral snapshot — not saved to transcript history.',
|
||||
status_no_tokens: 'No token data',
|
||||
status_profile: 'Profile',
|
||||
status_hermes_home: 'Hermes home',
|
||||
@@ -5387,6 +5399,8 @@ const LOCALES = {
|
||||
settings_tab_conversation: 'Conversation',
|
||||
settings_tab_preferences: 'Preferences',
|
||||
settings_tab_system: 'System',
|
||||
status_updated: 'Updated',
|
||||
status_ephemeral: 'Ephemeral snapshot — not saved to transcript history.',
|
||||
status_no_tokens: 'No token data',
|
||||
status_profile: 'Profile',
|
||||
status_hermes_home: 'Hermes home',
|
||||
@@ -6253,6 +6267,8 @@ const LOCALES = {
|
||||
status_hermes_home: 'Hermes 主目錄',
|
||||
status_started: '開始時間',
|
||||
status_tokens: 'Token',
|
||||
status_updated: '已更新',
|
||||
status_ephemeral: '临时快照 — 不会保存到对话历史。',
|
||||
status_no_tokens: '未使用 Token',
|
||||
status_unknown: '未知',
|
||||
status_completed: '\u5df2\u5b8c\u6210',
|
||||
@@ -6261,6 +6277,7 @@ const LOCALES = {
|
||||
status_load_failed: '\u8f09\u5165\u72c0\u614b\u5931\u6557\uff1a',
|
||||
status_messages: '\u8a0a\u606f\u6578',
|
||||
status_model: '\u6a21\u578b',
|
||||
status_provider: '供应商',
|
||||
status_no: '\u5426',
|
||||
status_personality: '\u4eba\u8a2d',
|
||||
status_session_id: '\u6703\u8a71 ID',
|
||||
@@ -6626,6 +6643,7 @@ const LOCALES = {
|
||||
status_session_id: 'ID da Sessão',
|
||||
status_title: 'Título',
|
||||
status_model: 'Modelo',
|
||||
status_provider: 'Provedor',
|
||||
status_workspace: 'Workspace',
|
||||
status_personality: 'Personalidade',
|
||||
status_messages: 'Mensagens',
|
||||
@@ -6634,6 +6652,8 @@ const LOCALES = {
|
||||
status_hermes_home: 'Diretório Hermes',
|
||||
status_started: 'Iniciado',
|
||||
status_tokens: 'Tokens',
|
||||
status_updated: 'Atualizado',
|
||||
status_ephemeral: 'Instantâneo efêmero — não salvo no histórico.',
|
||||
status_no_tokens: 'Nenhum token usado',
|
||||
status_unknown: 'Desconhecido',
|
||||
status_yes: 'Sim',
|
||||
@@ -7464,6 +7484,7 @@ const LOCALES = {
|
||||
status_session_id: '세션 ID',
|
||||
status_title: '제목',
|
||||
status_model: '모델',
|
||||
status_provider: '제공자',
|
||||
status_workspace: '워크스페이스',
|
||||
status_personality: '페르소나',
|
||||
status_messages: '메시지',
|
||||
@@ -7472,6 +7493,8 @@ const LOCALES = {
|
||||
status_hermes_home: 'Hermes 홈',
|
||||
status_started: '시작 시간',
|
||||
status_tokens: '토큰',
|
||||
status_updated: '업데이트됨',
|
||||
status_ephemeral: '임시 스냅샷 — 대화 기록에 저장되지 않습니다.',
|
||||
status_no_tokens: '사용된 토큰 없음',
|
||||
status_unknown: '알 수 없음',
|
||||
status_yes: '예',
|
||||
|
||||
@@ -2637,6 +2637,21 @@ main.main.showing-profiles > #mainProfiles{display:flex;}
|
||||
.msg-body:empty { display: none; }
|
||||
.assistant-turn { width: 100%; }
|
||||
.assistant-turn-blocks { display: flex; flex-direction: column; }
|
||||
.status-card{margin:8px 0 8px var(--msg-rail);max-width:min(var(--msg-max),760px);border:1px solid var(--border-subtle);background:var(--surface-subtle);border-radius:var(--radius-card);box-shadow:0 10px 24px rgba(0,0,0,.05);overflow:hidden;}
|
||||
.status-card-head{display:flex;align-items:flex-start;justify-content:space-between;gap:12px;padding:14px 16px;border-bottom:1px solid var(--border-subtle);background:linear-gradient(180deg,var(--surface-subtle-hover),var(--surface-subtle));}
|
||||
.status-card-title-wrap{min-width:0;}
|
||||
.status-card-title{font-weight:650;color:var(--text);font-size:14px;letter-spacing:.01em;}
|
||||
.status-card-subtitle{font-size:12px;color:var(--muted);margin-top:3px;}
|
||||
.status-card-session-copy{display:inline-flex;align-items:center;gap:7px;min-height:28px;padding:5px 9px;border:1px solid var(--border-subtle);border-radius:999px;background:var(--surface);color:var(--muted);font-size:12px;font-family:var(--font-mono);cursor:pointer;max-width:230px;}
|
||||
.status-card-session-copy span{overflow:hidden;text-overflow:ellipsis;white-space:nowrap;}
|
||||
.status-card-session-copy:hover,.status-card-session-copy.copied{color:var(--accent-text);border-color:var(--accent-bg-strong);background:var(--accent-bg);}
|
||||
.status-card-grid{display:grid;grid-template-columns:minmax(120px,180px) minmax(0,1fr);gap:0;border-top:0;}
|
||||
.status-card-row{display:contents;}
|
||||
.status-card-label,.status-card-value{padding:9px 16px;border-top:1px solid var(--border-subtle);font-size:13px;line-height:1.4;}
|
||||
.status-card-row:first-child .status-card-label,.status-card-row:first-child .status-card-value{border-top:0;}
|
||||
.status-card-label{color:var(--muted);font-weight:550;background:rgba(0,0,0,.015);}
|
||||
.status-card-value{color:var(--text);word-break:break-word;font-family:var(--font-mono);}
|
||||
@media (max-width:700px){.status-card{margin-left:0;}.status-card-head{flex-direction:column;}.status-card-session-copy{max-width:100%;}.status-card-grid{grid-template-columns:1fr;}.status-card-label{padding-bottom:2px;border-top:1px solid var(--border-subtle);}.status-card-value{padding-top:2px;border-top:0;}}
|
||||
.assistant-segment-anchor { display: none; }
|
||||
|
||||
/* ── Classic conversation layout: user right, half-width; assistant left ── */
|
||||
|
||||
+43
-5
@@ -50,7 +50,6 @@ function _setCompressionSessionLock(sid){
|
||||
window._compressionLockSid=sid||null;
|
||||
}
|
||||
const esc=s=>String(s??'').replace(/[&<>"']/g,c=>({'&':'&','<':'<','>':'>','"':'"',"'":'''}[c]));
|
||||
|
||||
/**
|
||||
* Render fenced code blocks inside user messages.
|
||||
* Extracts ```…``` fences, replaces them with placeholders,
|
||||
@@ -58,6 +57,7 @@ const esc=s=>String(s??'').replace(/[&<>"']/g,c=>({'&':'&','<':'<','>':'&
|
||||
* with the same <pre><code> pipeline used by renderMd().
|
||||
* All non-fenced text stays escaped (no bold/italic/link interpretation).
|
||||
*/
|
||||
|
||||
function _renderUserFencedBlocks(text){
|
||||
const stash=[];
|
||||
let s=String(text||'');
|
||||
@@ -89,6 +89,31 @@ function _renderUserFencedBlocks(text){
|
||||
s=s.replace(/\x00UF(\d+)\x00/g,(_,i)=>stash[+i]);
|
||||
return s;
|
||||
}
|
||||
function _statusCardHtml(card){
|
||||
card=card||{};
|
||||
const rows=Array.isArray(card.rows)?card.rows:[];
|
||||
const sessionId=String(card.sessionId||'');
|
||||
const shortSessionId=sessionId.length>22?`${sessionId.slice(0,10)}…${sessionId.slice(-8)}`:sessionId;
|
||||
const copyIcon=(typeof li==='function')?li('copy',13):'Copy';
|
||||
const copyBtn=sessionId
|
||||
? `<button class="status-card-session-copy" type="button" data-copy-status-session="${esc(card.sessionId||'')}" title="${esc(t('copy'))}" onclick="copyStatusSessionId(this);event.stopPropagation()"><span>${esc(shortSessionId)}</span>${copyIcon}</button>`
|
||||
: '';
|
||||
const rowHtml=rows.map(row=>`
|
||||
<div class="status-card-row">
|
||||
<span class="status-card-label">${esc(row.label||'')}</span>
|
||||
<span class="status-card-value">${esc(row.value||'')}</span>
|
||||
</div>`).join('');
|
||||
return `<div class="status-card" data-status-card="1">
|
||||
<div class="status-card-head">
|
||||
<div class="status-card-title-wrap">
|
||||
<div class="status-card-title">${esc(card.title||t('status_heading'))}</div>
|
||||
<div class="status-card-subtitle">${esc(card.subtitle||'')}</div>
|
||||
</div>
|
||||
${copyBtn}
|
||||
</div>
|
||||
<div class="status-card-grid">${rowHtml}</div>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
const MESSAGE_RENDER_WINDOW_DEFAULT=50;
|
||||
let _messageRenderWindowSid=null;
|
||||
@@ -2716,6 +2741,16 @@ function _fallbackCopy(text){
|
||||
finally{document.body.removeChild(ta);}
|
||||
});
|
||||
}
|
||||
function copyStatusSessionId(btn){
|
||||
const text=btn&&btn.getAttribute('data-copy-status-session');
|
||||
if(!text)return;
|
||||
_copyText(text).then(()=>{
|
||||
const orig=btn.innerHTML;
|
||||
btn.innerHTML=(typeof li==='function')?li('check',13):t('copied');
|
||||
btn.classList.add('copied');
|
||||
setTimeout(()=>{btn.innerHTML=orig;btn.classList.remove('copied');},1500);
|
||||
}).catch(()=>showToast(t('copy_failed')));
|
||||
}
|
||||
function copyMsg(btn){
|
||||
const row=btn.closest('[data-raw-text]');
|
||||
const text=row?row.dataset.rawText:'';
|
||||
@@ -3763,7 +3798,7 @@ function renderMessages(){
|
||||
const hasTu=Array.isArray(m.content)&&m.content.some(p=>p&&p.type==='tool_use');
|
||||
if(hasTc||hasTu||_messageHasReasoningPayload(m)) return true;
|
||||
}
|
||||
return msgContent(m)||m.attachments?.length;
|
||||
return m._statusCard||msgContent(m)||m.attachments?.length;
|
||||
});
|
||||
$('emptyState').style.display=(vis.length||preservedCompressionTaskMessages.length)?'none':'';
|
||||
inner.innerHTML='';
|
||||
@@ -3782,7 +3817,7 @@ function renderMessages(){
|
||||
if(_isPreservedCompressionTaskListMessage(m)){preservedCompressionRawIdxs.push(rawIdx);rawIdx++;continue;}
|
||||
const hasTc=Array.isArray(m.tool_calls)&&m.tool_calls.length>0;
|
||||
const hasTu=Array.isArray(m.content)&&m.content.some(p=>p&&p.type==='tool_use');
|
||||
if(msgContent(m)||m.attachments?.length||(m.role==='assistant'&&(hasTc||hasTu||_messageHasReasoningPayload(m)))) visWithIdx.push({m,rawIdx});
|
||||
if(msgContent(m)||m._statusCard||m.attachments?.length||(m.role==='assistant'&&(hasTc||hasTu||_messageHasReasoningPayload(m)))) visWithIdx.push({m,rawIdx});
|
||||
rawIdx++;
|
||||
}
|
||||
// Show a top affordance when earlier transcript content exists either in
|
||||
@@ -3889,6 +3924,7 @@ function renderMessages(){
|
||||
}).join('')}</div>`;
|
||||
}
|
||||
const bodyHtml = isUser ? _renderUserFencedBlocks(content) : renderMd(_stripXmlToolCallsDisplay(String(content)));
|
||||
const statusHtml = (!isUser&&m._statusCard) ? _statusCardHtml(m._statusCard) : '';
|
||||
const isEditableUser=isUser&&rawIdx===lastUserRawIdx;
|
||||
const editBtn = isEditableUser ? `<button class="msg-action-btn" title="${t('edit_message')}" onclick="editMessage(this)">${li('pencil',13)}</button>` : '';
|
||||
const undoBtn = isLastAssistant ? `<button class="msg-action-btn" title="${t('undo_exchange')}" onclick="undoLastExchange()">${li('undo',13)}</button>` : '';
|
||||
@@ -3954,8 +3990,10 @@ function renderMessages(){
|
||||
if(isSimplifiedToolCalling()) assistantThinking.set(rawIdx, thinkingText);
|
||||
else if(window._showThinking!==false) seg.insertAdjacentHTML('beforeend', _thinkingCardHtml(thinkingText));
|
||||
}
|
||||
const hasVisibleBody=!!(String(content||'').trim()||filesHtml);
|
||||
if(hasVisibleBody){
|
||||
const hasVisibleBody=!!(String(content||'').trim()||filesHtml||statusHtml);
|
||||
if(statusHtml){
|
||||
seg.insertAdjacentHTML('beforeend', statusHtml);
|
||||
}else if(hasVisibleBody){
|
||||
seg.insertAdjacentHTML('beforeend', `${filesHtml}<div class="msg-body">${bodyHtml}</div>${footHtml}`);
|
||||
}else if(!(thinkingText&&window._showThinking!==false&&!isSimplifiedToolCalling())){
|
||||
seg.classList.add('assistant-segment-anchor');
|
||||
|
||||
@@ -0,0 +1,97 @@
|
||||
"""Regression tests for issue #463: WebUI /status info card.
|
||||
|
||||
/status should be a client-handled slash command that renders a safe,
|
||||
ephemeral assistant-style card from already-loaded session/profile/model data.
|
||||
It must not round-trip through the agent or a status endpoint just to draw the
|
||||
card.
|
||||
"""
|
||||
import pathlib
|
||||
|
||||
|
||||
REPO_ROOT = pathlib.Path(__file__).parent.parent
|
||||
COMMANDS_JS = (REPO_ROOT / "static" / "commands.js").read_text(encoding="utf-8")
|
||||
UI_JS = (REPO_ROOT / "static" / "ui.js").read_text(encoding="utf-8")
|
||||
STYLE_CSS = (REPO_ROOT / "static" / "style.css").read_text(encoding="utf-8")
|
||||
I18N_JS = (REPO_ROOT / "static" / "i18n.js").read_text(encoding="utf-8")
|
||||
MESSAGES_JS = (REPO_ROOT / "static" / "messages.js").read_text(encoding="utf-8")
|
||||
|
||||
|
||||
def _function_body(src: str, name: str) -> str:
|
||||
marker = f"function {name}"
|
||||
start = src.index(marker)
|
||||
brace = src.index("{", start)
|
||||
depth = 0
|
||||
for idx in range(brace, len(src)):
|
||||
if src[idx] == "{":
|
||||
depth += 1
|
||||
elif src[idx] == "}":
|
||||
depth -= 1
|
||||
if depth == 0:
|
||||
return src[start:idx + 1]
|
||||
raise AssertionError(f"Could not extract {name}()")
|
||||
|
||||
|
||||
def test_status_command_is_registered_with_help_text():
|
||||
assert "{name:'status'" in COMMANDS_JS
|
||||
assert "desc:t('cmd_status')" in COMMANDS_JS
|
||||
assert "fn:cmdStatus" in COMMANDS_JS
|
||||
assert "cmd_status:'Show session info'" in I18N_JS
|
||||
|
||||
|
||||
def test_status_command_uses_client_state_not_status_endpoint():
|
||||
body = _function_body(COMMANDS_JS, "cmdStatus")
|
||||
assert "/api/session/status" not in body
|
||||
assert "api(" not in body
|
||||
assert "S.session" in body
|
||||
assert "S.activeProfile" in COMMANDS_JS
|
||||
assert "model_provider" in COMMANDS_JS
|
||||
assert "last_usage" in COMMANDS_JS
|
||||
|
||||
|
||||
def test_status_command_pushes_ephemeral_status_card_message():
|
||||
body = _function_body(COMMANDS_JS, "cmdStatus")
|
||||
assert "_statusCard" in body
|
||||
assert "_ephemeral:true" in body
|
||||
assert "renderMessages()" in body
|
||||
assert "_statusCardFromSession(S.session)" in body
|
||||
helper = _function_body(COMMANDS_JS, "_statusCardFromSession")
|
||||
assert "session_id" in helper
|
||||
assert "updated_at" in helper
|
||||
assert "message_count" in helper
|
||||
assert "active_stream_id" in helper
|
||||
|
||||
|
||||
def test_status_card_renderer_escapes_all_dynamic_values_and_is_copyable():
|
||||
body = _function_body(UI_JS, "_statusCardHtml")
|
||||
assert "data-status-card" in body
|
||||
assert "data-copy-status-session" in body
|
||||
assert "onclick=\"copyStatusSessionId(this);event.stopPropagation()\"" in body
|
||||
assert "esc(card.title" in body
|
||||
assert "esc(card.subtitle" in body
|
||||
assert "esc(row.label" in body
|
||||
assert "esc(row.value" in body
|
||||
assert "esc(card.sessionId" in body
|
||||
assert "renderMd(" not in body, "Status card data should not be interpreted as markdown"
|
||||
|
||||
|
||||
def test_render_messages_treats_status_card_as_visible_assistant_content():
|
||||
render_body = _function_body(UI_JS, "renderMessages")
|
||||
assert "m._statusCard" in render_body
|
||||
assert "_statusCardHtml(m._statusCard)" in render_body
|
||||
assert "statusHtml" in render_body
|
||||
|
||||
|
||||
def test_status_card_styles_exist():
|
||||
assert ".status-card" in STYLE_CSS
|
||||
assert ".status-card-grid" in STYLE_CSS
|
||||
assert ".status-card-session-copy" in STYLE_CSS
|
||||
|
||||
|
||||
def test_status_command_never_reaches_agent_send_path():
|
||||
send_body = _function_body(MESSAGES_JS, "send")
|
||||
branch_start = send_body.index("if(text.startsWith('/')")
|
||||
branch_end = send_body.index("if(_parsedCmd&&!_cmd)", branch_start)
|
||||
cmd_branch = send_body[branch_start:branch_end]
|
||||
assert "COMMANDS.find" in cmd_branch
|
||||
assert "return;" in cmd_branch
|
||||
assert "api('/api/chat/start'" not in cmd_branch
|
||||
Reference in New Issue
Block a user