Announce New Chat workspace transiently

This commit is contained in:
sheldon-im authored and t committed 2026-07-09 02:40:36 +00:00
1 parent 23de53c004
commit 06e0df4656
5 files changed
+127 -1

No files matched your search

+15
View File
@@ -215,6 +215,7 @@ const LOCALES = {
ws_search_placeholder: 'Search workspaces…',
ws_no_results: 'No workspaces found',
workspace_switcher_aria: 'Switch workspace. Current workspace: {0}.',
new_session_workspace_announce: 'New chat started in workspace: {0}.',
workspace_new_worktree_conversation: 'New conversation in worktree',
workspace_new_worktree_conversation_meta: 'Create an isolated git worktree for this workspace.',
workspace_worktree_created: 'Worktree conversation created',
@@ -1915,6 +1916,7 @@ const LOCALES = {
ws_search_placeholder: 'Cerca workspace…',
ws_no_results: 'Nessun workspace trovato',
workspace_switcher_aria: 'Cambia workspace. Workspace corrente: {0}.',
new_session_workspace_announce: 'Nuova chat avviata nel workspace: {0}.',
workspace_new_worktree_conversation: 'Nuova conversazione in worktree',
workspace_new_worktree_conversation_meta: 'Crea un worktree git isolato per questo workspace.',
workspace_worktree_created: 'Conversazione worktree creata',
@@ -3606,6 +3608,7 @@ const LOCALES = {
ws_search_placeholder: 'ワークスペースを検索…',
ws_no_results: 'ワークスペースが見つかりません',
workspace_switcher_aria: 'ワークスペースを切り替えます。現在のワークスペース: {0}。',
new_session_workspace_announce: 'ワークスペース {0} で新しいチャットを開始しました。',
workspace_new_worktree_conversation: 'worktree で新しい会話',
workspace_new_worktree_conversation_meta: 'このワークスペース用に隔離された git worktree を作成します。',
workspace_worktree_created: 'worktree 会話を作成しました',
@@ -5391,6 +5394,7 @@ const LOCALES = {
ws_search_placeholder: 'Поиск рабочих пространств…',
ws_no_results: 'Рабочие пространства не найдены',
workspace_switcher_aria: 'Переключить рабочее пространство. Текущее рабочее пространство: {0}.',
new_session_workspace_announce: 'Новый чат запущен в рабочем пространстве: {0}.',
workspace_new_worktree_conversation: 'Новый разговор в worktree',
workspace_new_worktree_conversation_meta: 'Создать изолированный git worktree для этого рабочего пространства.',
workspace_worktree_created: 'Разговор в worktree создан',
@@ -6909,6 +6913,7 @@ const LOCALES = {
ws_search_placeholder: 'Buscar espacios de trabajo…',
ws_no_results: 'No se encontraron espacios de trabajo',
workspace_switcher_aria: 'Cambiar espacio de trabajo. Espacio de trabajo actual: {0}.',
new_session_workspace_announce: 'Nuevo chat iniciado en el espacio de trabajo: {0}.',
workspace_new_worktree_conversation: 'Nueva conversación en worktree',
workspace_new_worktree_conversation_meta: 'Crear un git worktree aislado para este espacio de trabajo.',
workspace_worktree_created: 'Conversación en worktree creada',
@@ -9531,6 +9536,7 @@ const LOCALES = {
ws_search_placeholder: 'Arbeitsbereiche suchen…',
ws_no_results: 'Keine Arbeitsbereiche gefunden',
workspace_switcher_aria: 'Workspace wechseln. Aktueller Workspace: {0}.',
new_session_workspace_announce: 'Neuer Chat im Workspace gestartet: {0}.',
workspace_new_worktree_conversation: 'Neue Unterhaltung in Worktree',
workspace_new_worktree_conversation_meta: 'Erstellt einen isolierten git worktree für diesen Arbeitsbereich.',
workspace_worktree_created: 'Worktree-Unterhaltung erstellt',
@@ -10167,6 +10173,7 @@ const LOCALES = {
ws_search_placeholder: '搜索工作区…',
ws_no_results: '未找到工作区',
workspace_switcher_aria: '切换工作区。当前工作区:{0}。',
new_session_workspace_announce: '已在工作区 {0} 中开始新聊天。',
workspace_new_worktree_conversation: '在 worktree 中新建对话',
workspace_new_worktree_conversation_meta: '为此工作区创建隔离的 git worktree。',
workspace_worktree_created: '已创建 worktree 对话',
@@ -11818,6 +11825,7 @@ const LOCALES = {
ws_search_placeholder: '搜尋工作區…',
ws_no_results: '找不到工作區',
workspace_switcher_aria: '切換工作區。目前工作區:{0}。',
new_session_workspace_announce: '已在工作區 {0} 中開始新聊天。',
workspace_new_worktree_conversation: '在 worktree 中新增對話',
workspace_new_worktree_conversation_meta: '為此工作區建立隔離的 git worktree。',
workspace_worktree_created: '已建立 worktree 對話',
@@ -13445,6 +13453,7 @@ const LOCALES = {
ws_search_placeholder: 'Buscar espaços de trabalho…',
ws_no_results: 'Nenhum espaço de trabalho encontrado',
workspace_switcher_aria: 'Alternar espaço de trabalho. Espaço de trabalho atual: {0}.',
new_session_workspace_announce: 'Novo chat iniciado no espaço de trabalho: {0}.',
workspace_new_worktree_conversation: 'Nova conversa em worktree',
workspace_new_worktree_conversation_meta: 'Cria um git worktree isolado para este espaço de trabalho.',
workspace_worktree_created: 'Conversa em worktree criada',
@@ -15012,6 +15021,7 @@ const LOCALES = {
ws_search_placeholder: '워크스페이스 검색…',
ws_no_results: '워크스페이스를 찾을 수 없습니다',
workspace_switcher_aria: '워크스페이스 전환. 현재 워크스페이스: {0}.',
new_session_workspace_announce: '워크스페이스 {0}에서 새 채팅을 시작했습니다.',
workspace_new_worktree_conversation: 'worktree에서 새 대화',
workspace_new_worktree_conversation_meta: '이 워크스페이스용 격리된 git worktree를 만듭니다.',
workspace_worktree_created: 'worktree 대화가 생성되었습니다',
@@ -16696,6 +16706,7 @@ const LOCALES = {
ws_search_placeholder: 'Rechercher des espaces de travail\u2026',
ws_no_results: 'Aucun espace de travail trouvé',
workspace_switcher_aria: 'Changer d’espace de travail. Espace de travail actuel : {0}.',
new_session_workspace_announce: 'Nouvelle discussion démarrée dans l’espace de travail : {0}.',
workspace_new_worktree_conversation: 'Nouvelle conversation dans un worktree',
workspace_new_worktree_conversation_meta: 'Créer un worktree git isolé pour cet espace de travail.',
workspace_worktree_created: 'Conversation Worktree créée',
@@ -19799,6 +19810,7 @@ const LOCALES = {
workspace_worktree_failed: 'Vytvoření worktree selhalo: ',
ws_no_results: 'Nenalezeny žádné pracovní prostory',
workspace_switcher_aria: 'Přepnout pracovní prostor. Aktuální pracovní prostor: {0}.',
new_session_workspace_announce: 'Nový chat spuštěn v pracovním prostoru: {0}.',
ws_search_placeholder: 'Hledat pracovní prostory…',
you: 'Vy',
// tool/worklog references
@@ -20026,6 +20038,7 @@ const LOCALES = {
ws_search_placeholder: 'Çalışma alanlarını arayın\u2026',
ws_no_results: 'Çalışma alanı bulunamadı',
workspace_switcher_aria: 'Çalışma alanını değiştir. Geçerli çalışma alanı: {0}.',
new_session_workspace_announce: 'Yeni sohbet şu çalışma alanında başlatıldı: {0}.',
workspace_new_worktree_conversation: 'Worktree\'de yeni konuşma',
workspace_new_worktree_conversation_meta: 'Bu çalışma alanı için yalıtılmış bir git çalışma ağacı oluşturun.',
workspace_worktree_created: 'Worktree görüşmesi oluşturuldu',
@@ -21703,6 +21716,7 @@ const LOCALES = {
ws_search_placeholder: 'Szukaj obszarów roboczych…',
ws_no_results: 'Nie znaleziono żadnych obszarów roboczych',
workspace_switcher_aria: 'Przełącz obszar roboczy. Bieżący obszar roboczy: {0}.',
new_session_workspace_announce: 'Nowy czat rozpoczęty w obszarze roboczym: {0}.',
workspace_new_worktree_conversation: 'Nowa konwersacja w worktree',
workspace_new_worktree_conversation_meta: 'Utwórz izolowane git worktree dla tego obszaru roboczego.',
workspace_worktree_created: 'Utworzono konwersację w worktree',
@@ -23366,6 +23380,7 @@ const LOCALES = {
ws_search_placeholder: 'Tìm workspace…',
ws_no_results: 'Không tìm thấy workspace nào',
workspace_switcher_aria: 'Chuyển workspace. Workspace hiện tại: {0}.',
new_session_workspace_announce: 'Đã bắt đầu cuộc trò chuyện mới trong workspace: {0}.',
workspace_new_worktree_conversation: 'Cuộc trò chuyện mới trong worktree',
workspace_new_worktree_conversation_meta: 'Tạo git worktree độc lập cho workspace này.',
workspace_worktree_created: 'Đã tạo cuộc trò chuyện worktree',
+2
View File
@@ -449,6 +449,7 @@
</div>
<div class="messages-inner" id="msgInner"></div>
<div id="liveRunStatus" hidden></div>
<div id="a11yAnnouncer" class="sr-only" role="status" aria-live="polite" aria-atomic="true"></div>
<div id="liveCompressionCards" class="live-compression-cards"></div>
<div id="liveToolCards" style="display:none;max-width:800px;margin:0 auto;width:100%;padding:0 24px;"></div>
</div>
@@ -591,6 +592,7 @@
<span class="voice-mode-label" id="voiceModeLabel"></span>
</div>
<textarea id="msg" rows="1" placeholder="Message Hermes…"></textarea>
<div id="composerWorkspaceContext" class="sr-only"></div>
<div class="composer-footer">
<div class="composer-left">
<input type="file" id="fileInput" class="file-input-visually-hidden" multiple accept="image/*,text/*,application/pdf,application/json,application/vnd.ms-excel,application/vnd.openxmlformats-officedocument.spreadsheetml.sheet,application/msword,application/vnd.openxmlformats-officedocument.wordprocessingml.document,.md,.py,.js,.ts,.yaml,.yml,.toml,.csv,.sh,.txt,.log,.env,.xls,.xlsx,.doc,.docx,.zip,.tar,.gz,.tgz,.bz2,.xz">
+57
View File
@@ -1050,6 +1050,62 @@ function _clearEmptyComposerModelOverride(){
_emptyComposerModelOverrideHost._emptyComposerModelOverride=null;
}
let _newSessionWorkspaceAnnouncementClearTimer=null;
function _setNewSessionWorkspaceCue(message){
const announcer=$('a11yAnnouncer');
const composerCue=$('composerWorkspaceContext');
const msg=$('msg');
const cueId='composerWorkspaceContext';
if(_newSessionWorkspaceAnnouncementClearTimer&&typeof clearTimeout==='function'){
clearTimeout(_newSessionWorkspaceAnnouncementClearTimer);
_newSessionWorkspaceAnnouncementClearTimer=null;
}
const removeComposerCue=()=>{
if(composerCue&&composerCue.textContent===message) composerCue.textContent='';
if(msg){
const ids=(msg.getAttribute('aria-describedby')||'')
.split(/\s+/)
.filter(Boolean)
.filter(id=>id!==cueId);
if(ids.length) msg.setAttribute('aria-describedby',ids.join(' '));
else msg.removeAttribute('aria-describedby');
}
};
const clear=()=>{
if(announcer&&announcer.textContent===message) announcer.textContent='';
removeComposerCue();
_newSessionWorkspaceAnnouncementClearTimer=null;
};
const announce=()=>{
if(announcer) announcer.textContent=message;
if(composerCue&&msg){
composerCue.textContent=message;
const ids=(msg.getAttribute('aria-describedby')||'')
.split(/\s+/)
.filter(Boolean)
.filter(id=>id!==cueId);
ids.push(cueId);
msg.setAttribute('aria-describedby',ids.join(' '));
}
if(typeof setTimeout==='function'){
_newSessionWorkspaceAnnouncementClearTimer=setTimeout(clear,5000);
}
};
if(announcer) announcer.textContent='';
removeComposerCue();
if(typeof requestAnimationFrame==='function') requestAnimationFrame(announce);
else announce();
}
function _announceNewSessionWorkspace(session){
if(!session||!session.workspace) return;
const name=(typeof getWorkspaceFriendlyName==='function')
? getWorkspaceFriendlyName(session.workspace)
: String(session.workspace).split('/').filter(Boolean).pop()||session.workspace;
_setNewSessionWorkspaceCue(t('new_session_workspace_announce',name));
}
function _setNewSessionPending(pending){
const ids=['btnNewChat','btnTitlebarNewChat'];
for (let i=0;i<ids.length;i++){
@@ -1231,6 +1287,7 @@ async function newSession(flash, options={}){
}
updateQueueBadge(S.session.session_id);
syncTopbar();renderMessages();
if(typeof _announceNewSessionWorkspace==='function') _announceNewSessionWorkspace(S.session);
// Keep new-chat first paint instant. The workspace tree / git badge can
// refresh right after paint unless this caller explicitly needs it loaded
// before continuing (profile/default-workspace binding path).
+1
View File
@@ -1519,6 +1519,7 @@
}
}
html{background:var(--sidebar);}
.sr-only{position:absolute!important;width:1px!important;height:1px!important;padding:0!important;margin:-1px!important;overflow:hidden!important;clip:rect(0 0 0 0)!important;clip-path:inset(50%)!important;white-space:nowrap!important;border:0!important;}
body{background:var(--bg);color:var(--text);height:100vh;height:100dvh;overflow:hidden;display:flex;flex-direction:column;}
.layout{display:flex;width:100%;flex:1 1 auto;min-height:0;}
.app-titlebar{display:flex;align-items:center;justify-content:center;height:38px;flex-shrink:0;background:var(--sidebar);border-bottom:1px solid var(--border);padding:0 12px;padding-top:var(--app-titlebar-safe-top);padding-left:max(12px,env(safe-area-inset-left,0));padding-right:max(12px,env(safe-area-inset-right,0));box-sizing:content-box;font-size:12px;color:var(--muted);user-select:none;-webkit-app-region:drag;position:relative;z-index:20;}
@@ -47,5 +47,56 @@ def test_composer_workspace_dropdown_keeps_aria_expanded_in_sync():
assert "mobileAction.setAttribute('aria-expanded','false')" in close
def test_workspace_switcher_i18n_key_exists_in_english_locale():
def test_composer_workspace_cue_is_transient_not_a_persistent_editor_description():
assert 'id="msg"' in INDEX_HTML
assert 'aria-describedby="composerWorkspaceContext"' not in INDEX_HTML
assert 'id="composerWorkspaceContext"' in INDEX_HTML
assert 'class="sr-only"' in INDEX_HTML
sync = _block(PANELS_JS, "function syncWorkspaceDisplays", "async function loadWorkspaceList")
assert "composerWorkspaceContext" not in sync
assert "workspace_context_aria" not in sync
assert "workspace_context_none" not in sync
def test_new_chat_has_screen_reader_only_workspace_announcer():
assert 'id="a11yAnnouncer"' in INDEX_HTML
assert 'class="sr-only"' in INDEX_HTML
assert 'role="status"' in INDEX_HTML
assert 'aria-live="polite"' in INDEX_HTML
assert 'aria-atomic="true"' in INDEX_HTML
assert 'id="a11yAnnouncer" hidden' not in INDEX_HTML
assert 'id="a11yAnnouncer" aria-hidden="true"' not in INDEX_HTML
assert ".sr-only{" in STYLE_CSS
sr_only = _block(STYLE_CSS, ".sr-only{", "body{")
assert "display:none" not in sr_only
assert "visibility:hidden" not in sr_only
assert "clip-path:inset(50%)" in sr_only
def test_new_session_announces_started_workspace_without_leaving_stale_browse_text():
helper = _block(SESSIONS_JS, "function _setNewSessionWorkspaceCue", "function _setNewSessionPending")
new_session = _block(SESSIONS_JS, "async function newSession", "/**\n * Self-heal")
assert "const announcer=$('a11yAnnouncer')" in helper
assert "const composerCue=$('composerWorkspaceContext')" in helper
assert "composerCue.textContent=message" in helper
assert "msg.setAttribute('aria-describedby',ids.join(' '))" in helper
assert "announcer.textContent=message" in helper
assert "setTimeout(clear,5000)" in helper
assert "announcer.textContent=''" in helper
assert "composerCue.textContent=''" in helper
assert "msg.removeAttribute('aria-describedby')" in helper
assert "getWorkspaceFriendlyName(session.workspace)" in helper
assert "t('new_session_workspace_announce',name)" in helper
assert "typeof requestAnimationFrame==='function'" in helper
assert "if(typeof _announceNewSessionWorkspace==='function') _announceNewSessionWorkspace(S.session);" in new_session
assert new_session.index("S.session=data.session") < new_session.index("_announceNewSessionWorkspace(S.session);")
def test_workspace_a11y_i18n_keys_exist_in_english_locale():
assert "workspace_switcher_aria: 'Switch workspace. Current workspace: {0}.'" in I18N_JS
assert "workspace_context_aria" not in I18N_JS
assert "workspace_context_none" not in I18N_JS
assert "new_session_workspace_announce: 'New chat started in workspace: {0}.'" in I18N_JS