Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Binary file added docs/pr-media/463/status-command-card.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
88 changes: 60 additions & 28 deletions static/commands.js
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down
23 changes: 23 additions & 0 deletions static/i18n.js
Original file line number Diff line number Diff line change
Expand Up @@ -225,14 +225,17 @@ 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',
status_agent_running:'Agent running',
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',
Expand Down Expand Up @@ -1127,6 +1130,7 @@ const LOCALES = {
status_session_id:'セッションID',
status_title:'タイトル',
status_model:'モデル',
status_provider:'プロバイダー',
status_workspace:'ワークスペース',
status_personality:'パーソナリティ',
status_messages:'メッセージ',
Expand All @@ -1135,6 +1139,8 @@ const LOCALES = {
status_hermes_home: 'Hermes ホーム',
status_started: '開始',
status_tokens: 'トークン',
status_updated: '更新',
status_ephemeral: '一時的なスナップショット — 履歴には保存されません。',
status_no_tokens: 'トークン未使用',
status_unknown: '不明',
status_yes:'はい',
Expand Down Expand Up @@ -2540,6 +2546,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',
Expand Down Expand Up @@ -3366,6 +3374,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',
Expand Down Expand Up @@ -3939,6 +3949,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',
Expand Down Expand Up @@ -5039,6 +5051,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',
Expand Down Expand Up @@ -5905,6 +5919,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',
Expand All @@ -5913,6 +5929,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',
Expand Down Expand Up @@ -6278,6 +6295,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',
Expand All @@ -6286,6 +6304,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',
Expand Down Expand Up @@ -7058,6 +7078,7 @@ const LOCALES = {
status_session_id: '세션 ID',
status_title: '제목',
status_model: '모델',
status_provider: '제공자',
status_workspace: '워크스페이스',
status_personality: '페르소나',
status_messages: '메시지',
Expand All @@ -7066,6 +7087,8 @@ const LOCALES = {
status_hermes_home: 'Hermes 홈',
status_started: '시작 시간',
status_tokens: '토큰',
status_updated: '업데이트됨',
status_ephemeral: '임시 스냅샷 — 대화 기록에 저장되지 않습니다.',
status_no_tokens: '사용된 토큰 없음',
status_unknown: '알 수 없음',
status_yes: '예',
Expand Down
15 changes: 15 additions & 0 deletions static/style.css
Original file line number Diff line number Diff line change
Expand Up @@ -2635,6 +2635,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 ── */
Expand Down
48 changes: 43 additions & 5 deletions static/ui.js
Original file line number Diff line number Diff line change
Expand Up @@ -50,14 +50,14 @@ function _setCompressionSessionLock(sid){
window._compressionLockSid=sid||null;
}
const esc=s=>String(s??'').replace(/[&<>"']/g,c=>({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#39;'}[c]));

/**
* Render fenced code blocks inside user messages.
* Extracts ```…``` fences, replaces them with placeholders,
* escapes remaining text as plain HTML, then restores code blocks
* 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||'');
Expand Down Expand Up @@ -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>`;
}

/* ── Image lightbox — click any .msg-media-img to enlarge ─────────────────── */
function _openImgLightbox(src, alt) {
Expand Down Expand Up @@ -2668,6 +2693,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:'';
Expand Down Expand Up @@ -3712,7 +3747,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='';
Expand Down Expand Up @@ -3740,7 +3775,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++;
}
let lastUserRawIdx=-1;
Expand Down Expand Up @@ -3821,6 +3856,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>` : '';
Expand Down Expand Up @@ -3886,8 +3922,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');
Expand Down
Loading
Loading