2026-05-31 23:58:26 +09:00
// Personal Assistant — sidebar entry, settings modal, and chat-header extras.
//
// The Assistant is just a specially-flagged CrewMember whose pinned Session
// lives alongside normal chats. The sidebar button resolves the per-user
// singleton via /api/assistant/session and hands it to selectSession() so we
// reuse the full existing chat render path.
import uiModule from './ui.js' ;
import { selectSession } from './sessions.js' ;
2026-06-01 15:26:10 +10:00
import { sortModelIds } from './modelSort.js' ;
2026-05-31 23:58:26 +09:00
const API = '/api/assistant' ;
let _cachedSettings = null ; // most recent GET /api/assistant/settings payload
let _modalEl = null ;
async function _fetchJSON ( url , opts = { } ) {
const res = await fetch ( url , { credentials : 'same-origin' , ... opts } ) ;
if ( ! res . ok ) throw new Error ( ` ${ url } → ${ res . status } ` ) ;
return res . json ( ) ;
}
export async function openAssistantChat ( ) {
try {
const info = await _fetchJSON ( ` ${ API } /session ` ) ;
if ( ! info ? . session _id ) {
uiModule . showToast ( 'Assistant session unavailable' ) ;
return ;
}
await selectSession ( info . session _id ) ;
// Refresh settings cache so the header buttons / gear act on fresh data.
_cachedSettings = null ;
} catch ( e ) {
console . error ( 'openAssistantChat failed:' , e ) ;
uiModule . showToast ( 'Could not open assistant' ) ;
}
}
async function _getSettings ( force = false ) {
if ( ! force && _cachedSettings ) return _cachedSettings ;
_cachedSettings = await _fetchJSON ( ` ${ API } /settings ` ) ;
return _cachedSettings ;
}
async function _saveSettings ( payload ) {
const res = await fetch ( ` ${ API } /settings ` , {
method : 'PATCH' ,
credentials : 'same-origin' ,
headers : { 'Content-Type' : 'application/json' } ,
body : JSON . stringify ( payload ) ,
} ) ;
if ( ! res . ok ) throw new Error ( ` PATCH ${ API } /settings → ${ res . status } ` ) ;
_cachedSettings = await res . json ( ) ;
return _cachedSettings ;
}
async function _listTimezones ( ) {
try {
const { timezones } = await _fetchJSON ( ` ${ API } /available-timezones ` ) ;
return timezones || [ 'UTC' ] ;
} catch {
return [ 'UTC' ] ;
}
}
async function _runCheckInNow ( taskId ) {
try {
await fetch ( ` ${ API } /run/ ${ encodeURIComponent ( taskId ) } ` , {
method : 'POST' ,
credentials : 'same-origin' ,
} ) ;
uiModule . showToast ( 'Check-in running…' ) ;
} catch ( e ) {
console . error ( e ) ;
uiModule . showToast ( 'Could not run check-in' ) ;
}
}
// ── Settings modal ─────────────────────────────────────────────────────────
function _closeModal ( ) {
if ( _modalEl ) {
_modalEl . classList . add ( 'hidden' ) ;
_modalEl . style . display = '' ;
}
}
function _ensureModalEl ( ) {
if ( _modalEl ) return _modalEl ;
const modal = document . createElement ( 'div' ) ;
modal . id = 'assistant-settings-modal' ;
modal . className = 'modal hidden' ;
modal . innerHTML = `
< div class = "modal-content" style = "max-width:640px;width:96%;" >
< div class = "modal-header" >
< h4 >
< svg width = "14" height = "14" viewBox = "0 0 24 24" fill = "none" stroke = "currentColor" stroke - width = "2" stroke - linecap = "round" stroke - linejoin = "round" style = "vertical-align:-2px;margin-right:6px;" > < circle cx = "12" cy = "8" r = "4" / > < path d = "M4 21a8 8 0 0 1 16 0" / > < / s v g >
Assistant settings
< / h 4 >
< button class = "close-btn" id = "assistant-settings-close" > ✖ < / b u t t o n >
< / d i v >
< div class = "modal-body" id = "assistant-settings-body" >
< div class = "hwfit-loading" > Loading … < / d i v >
< / d i v >
< / d i v > ` ;
document . body . appendChild ( modal ) ;
modal . querySelector ( '#assistant-settings-close' ) . addEventListener ( 'click' , _closeModal ) ;
modal . addEventListener ( 'click' , ( e ) => {
if ( e . target === modal ) _closeModal ( ) ;
} ) ;
_modalEl = modal ;
return modal ;
}
function _esc ( s ) {
return ( s || '' ) . replace ( /[&<>"']/g , ( c ) => ( {
'&' : '&' , '<' : '<' , '>' : '>' , '"' : '"' , "'" : ''' ,
} [ c ] ) ) ;
}
// Tool groups for the tool selector UI
const TOOL _GROUPS = {
'Email' : [ 'list_emails' , 'read_email' , 'send_email' , 'reply_to_email' , 'archive_email' , 'delete_email' , 'mark_email_read' ] ,
'Calendar & Notes' : [ 'manage_calendar' , 'manage_notes' , 'manage_tasks' ] ,
'Knowledge' : [ 'web_search' , 'read_file' , 'manage_memory' , 'manage_rag' , 'search_chats' ] ,
'Code' : [ 'bash' , 'python' , 'write_file' ] ,
'Documents' : [ 'create_document' , 'edit_document' , 'update_document' , 'suggest_document' ] ,
2026-06-18 07:56:37 +02:00
'AI & Models' : [ 'chat_with_model' , 'ask_teacher' , 'pipeline' , 'list_models' , 'generate_image' ] ,
2026-05-31 23:58:26 +09:00
'System' : [ 'manage_session' , 'manage_endpoints' , 'manage_mcp' , 'manage_settings' , 'manage_skills' , 'manage_webhooks' , 'manage_tokens' , 'manage_documents' , 'create_session' , 'list_sessions' , 'send_to_session' , 'ui_control' ] ,
} ;
async function _fetchEndpoints ( ) {
try {
const eps = await _fetchJSON ( '/api/model-endpoints' ) ;
return Array . isArray ( eps ) ? eps : [ ] ;
} catch { return [ ] ; }
}
function _renderSettingsBody ( body , data , tzList ) {
const crew = data . crew || { } ;
const checkIns = data . check _ins || [ ] ;
const enabledTools = new Set ( crew . enabled _tools || [ ] ) ;
const tzOptions = tzList . map ( ( z ) =>
` <option value=" ${ _esc ( z ) } " ${ z === crew . timezone ? ' selected' : '' } > ${ _esc ( z ) } </option> `
) . join ( '' ) ;
const checkInsHTML = checkIns . map ( ( c ) => `
< div class = "assistant-checkin-row" data - task - id = "${_esc(c.id)}" >
< div class = "assistant-checkin-head" >
< input type = "checkbox" class = "assistant-checkin-enabled" $ { c . enabled ? 'checked' : '' } title = "Enable this check-in" / >
< input type = "text" class = "assistant-checkin-name" value = "${_esc(c.name)}" placeholder = "Name" / >
< input type = "time" class = "assistant-checkin-time" value = "${_esc(c.scheduled_time || '')}" / >
< button type = "button" class = "assistant-checkin-run" title = "Run now" > Run now < / b u t t o n >
< / d i v >
< textarea class = "assistant-checkin-prompt" rows = "3" placeholder = "Prompt for this check-in" > $ { _esc ( c . prompt || '' ) } < / t e x t a r e a >
< div class = "assistant-checkin-meta" >
$ { c . next _run ? ` next run: ${ _esc ( c . next _run ) } ` : '' }
$ { c . last _run ? ` · last run: ${ _esc ( c . last _run ) } ` : '' }
$ { typeof c . run _count === 'number' ? ` · ${ c . run _count } runs ` : '' }
< / d i v >
< / d i v > ` ) . j o i n ( ' ' ) ;
// Tool selector grouped by category
let toolsHTML = '' ;
for ( const [ group , tools ] of Object . entries ( TOOL _GROUPS ) ) {
toolsHTML += ` <div class="assistant-tool-group"><span class="assistant-tool-group-label"> ${ _esc ( group ) } </span> ` ;
for ( const t of tools ) {
const checked = enabledTools . has ( t ) ? ' checked' : '' ;
const label = t . replace ( /_/g , ' ' ) ;
toolsHTML += ` <label class="assistant-tool-item"><input type="checkbox" class="assistant-tool-cb" value=" ${ _esc ( t ) } " ${ checked } /><span> ${ _esc ( label ) } </span></label> ` ;
}
toolsHTML += '</div>' ;
}
body . innerHTML = `
< div class = "assistant-settings-form" >
< label class = "assistant-field" >
< span > Name < / s p a n >
< input type = "text" id = "assistant-name" value = "${_esc(crew.name)}" placeholder = "Assistant" / >
< / l a b e l >
< div class = "assistant-field" >
< span style = "display:flex;align-items:center;gap:8px;" > Personality
< select id = "assistant-character-pick" style = "font-size:11px;padding:1px 6px;border:1px solid var(--border);border-radius:3px;background:var(--bg);color:var(--fg);max-width:180px;" >
Rename Character copy to Persona
Issue #234: the "Character" tab and its "Style of response" label made it
unclear that this is where a system prompt is set. Rename the user-facing
labels for clarity:
- "Character" tab + section heading -> "Persona"
- "Style of response" -> "System prompt"
- supporting strings: select placeholder, name placeholder, button/title
text, toasts, confirm/notice text, the chat-bar indicator tooltip, the
settings visibility toggle, and the assistant personality picker
("Characters" optgroup -> "Personas").
Used "Persona" rather than the issue's suggested "Preset" because the app
already has a distinct, user-facing "Presets" concept (built-in presets
like Code Analyze/Brainstorm/Reason, shown as their own group in the
assistant picker). "Persona" matches what this tab actually creates -- a
named persona with its own memories -- without colliding with that term.
Internal identifiers (element IDs, data-chartab attributes, function names)
and the character_name backend field are intentionally left unchanged so
existing saved presets and JS wiring keep working.
2026-06-02 05:42:15 +02:00
< option value = "" > -- pick from persona -- < / o p t i o n >
2026-05-31 23:58:26 +09:00
< / s e l e c t >
< / s p a n >
< textarea id = "assistant-personality" rows = "6" placeholder = "Describe the assistant's personality, tone, and behavior..." > $ { _esc ( crew . personality || '' ) } < / t e x t a r e a >
< / d i v >
< div class = "assistant-field-row" >
< label class = "assistant-field" >
< span > Timezone < / s p a n >
< select id = "assistant-timezone" >
< option value = "" $ { ! crew . timezone ? ' selected' : '' } > ( default -- UTC ) < / o p t i o n >
$ { tzOptions }
< / s e l e c t >
< / l a b e l >
< / d i v >
< div class = "assistant-field-row" >
< label class = "assistant-field" style = "flex:1;" >
< span > Model endpoint < / s p a n >
< select id = "assistant-endpoint" style = "width:100%;" >
< option value = "" > ( loading ... ) < / o p t i o n >
< / s e l e c t >
< / l a b e l >
< label class = "assistant-field" style = "flex:1;" >
< span > Model < / s p a n >
< select id = "assistant-model" style = "width:100%;" >
< option value = "${_esc(crew.model || '')}" > $ { _esc ( crew . model || '(default)' ) } < / o p t i o n >
< / s e l e c t >
< / l a b e l >
< / d i v >
< div class = "assistant-field" >
< span style = "display:flex;align-items:center;gap:8px;" > Tools
< button type = "button" id = "assistant-tools-all" class = "assistant-tools-toggle" style = "font-size:10px;opacity:0.5;cursor:pointer;background:none;border:1px solid var(--border);border-radius:3px;padding:1px 6px;" > all < / b u t t o n >
< button type = "button" id = "assistant-tools-none" class = "assistant-tools-toggle" style = "font-size:10px;opacity:0.5;cursor:pointer;background:none;border:1px solid var(--border);border-radius:3px;padding:1px 6px;" > none < / b u t t o n >
< / s p a n >
< div class = "assistant-tools-grid" id = "assistant-tools-grid" >
$ { toolsHTML }
< / d i v >
< / d i v >
< div class = "assistant-checkins" >
< h5 > Daily check - ins < / h 5 >
$ { checkInsHTML || '<div style="opacity:0.6;">No check-ins configured.</div>' }
< / d i v >
< div class = "assistant-settings-actions" >
< button type = "button" class = "cal-btn" id = "assistant-settings-cancel" > Cancel < / b u t t o n >
< button type = "button" class = "cal-btn cal-btn-primary" id = "assistant-settings-save" > Save < / b u t t o n >
< / d i v >
< / d i v >
` ;
// ── Populate model/endpoint dropdowns ──
const epSelect = body . querySelector ( '#assistant-endpoint' ) ;
const modelSelect = body . querySelector ( '#assistant-model' ) ;
_fetchEndpoints ( ) . then ( endpoints => {
let epHTML = '<option value="">(use session default)</option>' ;
for ( const ep of endpoints ) {
if ( ! ep . is _enabled ) continue ;
const url = ep . base _url || '' ;
const name = ep . name || url ;
const sel = ( crew . endpoint _url && url . includes ( crew . endpoint _url . replace ( '/v1' , '' ) . replace ( /\/$/ , '' ) ) ) ? ' selected' : '' ;
epHTML += ` <option value=" ${ _esc ( url ) } " ${ sel } > ${ _esc ( name ) } </option> ` ;
}
epSelect . innerHTML = epHTML ;
// When endpoint changes, load its models
epSelect . addEventListener ( 'change' , async ( ) => {
const url = epSelect . value ;
if ( ! url ) { modelSelect . innerHTML = '<option value="">(default)</option>' ; return ; }
const ep = endpoints . find ( e => e . base _url === url ) ;
if ( ! ep ) return ;
modelSelect . innerHTML = '<option value="">loading...</option>' ;
try {
const models = await _fetchJSON ( ` /api/model-endpoints/ ${ ep . id } /models ` ) ;
let mHTML = '' ;
2026-06-01 15:26:10 +10:00
const modelIds = ( models . models || models || [ ] ) . map ( m => typeof m === 'string' ? m : ( m . id || m . name || '' ) ) . filter ( Boolean ) ;
for ( const mid of sortModelIds ( modelIds ) ) {
2026-05-31 23:58:26 +09:00
const sel = mid === crew . model ? ' selected' : '' ;
mHTML += ` <option value=" ${ _esc ( mid ) } " ${ sel } > ${ _esc ( mid . split ( '/' ) . pop ( ) ) } </option> ` ;
}
modelSelect . innerHTML = mHTML || '<option value="">(no models)</option>' ;
} catch { modelSelect . innerHTML = '<option value="">(failed)</option>' ; }
} ) ;
// Trigger initial model load if endpoint is pre-selected
if ( epSelect . value ) epSelect . dispatchEvent ( new Event ( 'change' ) ) ;
} ) ;
// ── Tool toggle buttons ──
body . querySelector ( '#assistant-tools-all' ) ? . addEventListener ( 'click' , ( ) => {
body . querySelectorAll ( '.assistant-tool-cb' ) . forEach ( cb => { cb . checked = true ; } ) ;
} ) ;
body . querySelector ( '#assistant-tools-none' ) ? . addEventListener ( 'click' , ( ) => {
body . querySelectorAll ( '.assistant-tool-cb' ) . forEach ( cb => { cb . checked = false ; } ) ;
} ) ;
// ── Character picker — populate from presets + templates ──
const charPick = body . querySelector ( '#assistant-character-pick' ) ;
const personalityEl = body . querySelector ( '#assistant-personality' ) ;
if ( charPick && personalityEl ) {
( async ( ) => {
try {
const [ presetsRaw , templates ] = await Promise . all ( [
_fetchJSON ( '/api/presets' ) . catch ( ( ) => ( { } ) ) ,
_fetchJSON ( '/api/presets/templates' ) . catch ( ( ) => [ ] ) ,
] ) ;
// Presets API returns a dict keyed by preset ID, not an array
const allPresets = [ ] ;
if ( presetsRaw && typeof presetsRaw === 'object' && ! Array . isArray ( presetsRaw ) ) {
for ( const [ key , val ] of Object . entries ( presetsRaw ) ) {
if ( val && typeof val === 'object' && val . system _prompt ) {
allPresets . push ( { ... val , _key : key } ) ;
}
}
} else if ( Array . isArray ( presetsRaw ) ) {
allPresets . push ( ... presetsRaw ) ;
}
const allTemplates = Array . isArray ( templates ) ? templates : [ ] ;
Rename Character copy to Persona
Issue #234: the "Character" tab and its "Style of response" label made it
unclear that this is where a system prompt is set. Rename the user-facing
labels for clarity:
- "Character" tab + section heading -> "Persona"
- "Style of response" -> "System prompt"
- supporting strings: select placeholder, name placeholder, button/title
text, toasts, confirm/notice text, the chat-bar indicator tooltip, the
settings visibility toggle, and the assistant personality picker
("Characters" optgroup -> "Personas").
Used "Persona" rather than the issue's suggested "Preset" because the app
already has a distinct, user-facing "Presets" concept (built-in presets
like Code Analyze/Brainstorm/Reason, shown as their own group in the
assistant picker). "Persona" matches what this tab actually creates -- a
named persona with its own memories -- without colliding with that term.
Internal identifiers (element IDs, data-chartab attributes, function names)
and the character_name backend field are intentionally left unchanged so
existing saved presets and JS wiring keep working.
2026-06-02 05:42:15 +02:00
let opts = '<option value="">-- pick from persona --</option>' ;
2026-05-31 23:58:26 +09:00
if ( allPresets . length ) {
opts += '<optgroup label="Presets">' ;
for ( const p of allPresets ) {
if ( ! p . system _prompt ) continue ;
const name = p . character _name || p . name || p . _key || 'Unnamed' ;
opts += ` <option value="preset: ${ _esc ( p . _key || p . name || '' ) } "> ${ _esc ( name ) } </option> ` ;
}
opts += '</optgroup>' ;
}
if ( allTemplates . length ) {
Rename Character copy to Persona
Issue #234: the "Character" tab and its "Style of response" label made it
unclear that this is where a system prompt is set. Rename the user-facing
labels for clarity:
- "Character" tab + section heading -> "Persona"
- "Style of response" -> "System prompt"
- supporting strings: select placeholder, name placeholder, button/title
text, toasts, confirm/notice text, the chat-bar indicator tooltip, the
settings visibility toggle, and the assistant personality picker
("Characters" optgroup -> "Personas").
Used "Persona" rather than the issue's suggested "Preset" because the app
already has a distinct, user-facing "Presets" concept (built-in presets
like Code Analyze/Brainstorm/Reason, shown as their own group in the
assistant picker). "Persona" matches what this tab actually creates -- a
named persona with its own memories -- without colliding with that term.
Internal identifiers (element IDs, data-chartab attributes, function names)
and the character_name backend field are intentionally left unchanged so
existing saved presets and JS wiring keep working.
2026-06-02 05:42:15 +02:00
opts += '<optgroup label="Personas">' ;
2026-05-31 23:58:26 +09:00
for ( const t of allTemplates ) {
if ( ! t . system _prompt && ! t . personality ) continue ;
const name = t . character _name || t . name || 'Unnamed' ;
opts += ` <option value="template: ${ _esc ( t . id || t . name || '' ) } "> ${ _esc ( name ) } </option> ` ;
}
opts += '</optgroup>' ;
}
charPick . innerHTML = opts ;
charPick . _presets = allPresets ;
charPick . _templates = allTemplates ;
} catch { }
} ) ( ) ;
charPick . addEventListener ( 'change' , ( ) => {
const val = charPick . value ;
if ( ! val ) return ;
const [ type , id ] = val . split ( ':' , 2 ) ;
let prompt = '' ;
let name = '' ;
if ( type === 'preset' ) {
const p = ( charPick . _presets || [ ] ) . find ( x => ( x . _key || x . name || x . id ) === id ) ;
if ( p ) { prompt = p . system _prompt || p . personality || '' ; name = p . character _name || p . name || p . _key || '' ; }
} else if ( type === 'template' ) {
const t = ( charPick . _templates || [ ] ) . find ( x => ( x . id || x . name ) === id ) ;
if ( t ) { prompt = t . system _prompt || t . personality || '' ; name = t . character _name || t . name || '' ; }
}
if ( prompt ) personalityEl . value = prompt ;
const nameEl = body . querySelector ( '#assistant-name' ) ;
if ( name && nameEl ) nameEl . value = name ;
charPick . selectedIndex = 0 ;
} ) ;
}
// ── Event wiring ──
body . querySelector ( '#assistant-settings-cancel' ) . addEventListener ( 'click' , _closeModal ) ;
body . querySelector ( '#assistant-settings-save' ) . addEventListener ( 'click' , async ( ) => {
const selectedTools = [ ] ;
body . querySelectorAll ( '.assistant-tool-cb:checked' ) . forEach ( cb => selectedTools . push ( cb . value ) ) ;
const payload = {
name : body . querySelector ( '#assistant-name' ) . value . trim ( ) ,
personality : body . querySelector ( '#assistant-personality' ) . value ,
timezone : body . querySelector ( '#assistant-timezone' ) . value || null ,
model : body . querySelector ( '#assistant-model' ) . value || null ,
endpoint _url : body . querySelector ( '#assistant-endpoint' ) . value || null ,
enabled _tools : selectedTools ,
check _ins : Array . from ( body . querySelectorAll ( '.assistant-checkin-row' ) ) . map ( ( row ) => ( {
id : row . dataset . taskId ,
name : row . querySelector ( '.assistant-checkin-name' ) . value . trim ( ) ,
scheduled _time : row . querySelector ( '.assistant-checkin-time' ) . value ,
prompt : row . querySelector ( '.assistant-checkin-prompt' ) . value ,
enabled : row . querySelector ( '.assistant-checkin-enabled' ) . checked ,
} ) ) ,
} ;
try {
await _saveSettings ( payload ) ;
uiModule . showToast ( 'Assistant settings saved' ) ;
_closeModal ( ) ;
} catch ( e ) {
console . error ( e ) ;
uiModule . showToast ( 'Save failed' ) ;
}
} ) ;
body . querySelectorAll ( '.assistant-checkin-run' ) . forEach ( ( btn ) => {
btn . addEventListener ( 'click' , async ( e ) => {
e . stopPropagation ( ) ;
const row = btn . closest ( '.assistant-checkin-row' ) ;
if ( ! row ? . dataset . taskId ) return ;
const taskId = row . dataset . taskId ;
btn . disabled = true ;
btn . textContent = 'Running...' ;
await _runCheckInNow ( taskId ) ;
_closeModal ( ) ;
// Poll until done, then navigate to assistant chat
const sid = _cachedSettings ? . crew ? . session _id ;
const _poll = setInterval ( async ( ) => {
try {
const res = await fetch ( ` ${ API } /run-status/ ${ encodeURIComponent ( taskId ) } ` , { credentials : 'same-origin' } ) ;
if ( ! res . ok ) return ;
const data = await res . json ( ) ;
if ( data . status === 'done' || data . status === 'error' ) {
clearInterval ( _poll ) ;
// Hard navigate to force full reload of the session
if ( sid ) {
window . location . href = window . location . pathname + '#' + sid ;
window . location . reload ( ) ;
}
}
} catch { }
} , 2000 ) ;
setTimeout ( ( ) => clearInterval ( _poll ) , 90000 ) ;
} ) ;
} ) ;
}
export async function openAssistantSettings ( ) {
const modal = _ensureModalEl ( ) ;
modal . classList . remove ( 'hidden' ) ;
modal . style . display = 'flex' ;
const body = modal . querySelector ( '#assistant-settings-body' ) ;
body . innerHTML = '<div class="hwfit-loading">Loading…</div>' ;
try {
const [ data , tzList ] = await Promise . all ( [ _getSettings ( true ) , _listTimezones ( ) ] ) ;
_renderSettingsBody ( body , data , tzList ) ;
} catch ( e ) {
console . error ( e ) ;
body . innerHTML = '<div style="padding:12px;opacity:0.6;">Could not load assistant settings.</div>' ;
}
}
// Sidebar wiring removed — Assistant chat + settings now live as
// Activity / Settings tabs inside the Tasks modal (see tasks.js). The
// exports below are still used by tasks.js to surface those views.
// ── Chat-header affordances when the assistant session is active ───────────
async function _ensureHeaderAffordances ( sessionId ) {
try {
const settings = await _getSettings ( ) ;
if ( settings ? . crew ? . session _id !== sessionId ) return ;
} catch {
return ;
}
const headerRight = document . querySelector ( '.chat-header-right, #chat-header .actions, .chat-header' ) ;
if ( ! headerRight ) return ;
if ( headerRight . querySelector ( '#assistant-header-gear' ) ) return ;
const gear = document . createElement ( 'button' ) ;
gear . id = 'assistant-header-gear' ;
gear . type = 'button' ;
gear . title = 'Assistant settings' ;
gear . className = 'chat-header-btn' ;
gear . innerHTML = '<svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="3"/><path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 0 1 0 2.83 2 2 0 0 1-2.83 0l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-2 2 2 2 0 0 1-2-2v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 0 1-2.83 0 2 2 0 0 1 0-2.83l.06-.06a1.65 1.65 0 0 0 .33-1.82 1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1-2-2 2 2 0 0 1 2-2h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 0 1 0-2.83 2 2 0 0 1 2.83 0l.06.06a1.65 1.65 0 0 0 1.82.33H9a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 2-2 2 2 0 0 1 2 2v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 0 1 2.83 0 2 2 0 0 1 0 2.83l-.06.06a1.65 1.65 0 0 0-.33 1.82V9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 2 2 2 2 0 0 1-2 2h-.09a1.65 1.65 0 0 0-1.51 1z"/></svg>' ;
gear . addEventListener ( 'click' , openAssistantSettings ) ;
headerRight . appendChild ( gear ) ;
}
// Run a short polling check after session loads so we can add the gear button
// once the chat header DOM is in place. Fire-and-forget.
function _watchForAssistantActivation ( ) {
let retries = 0 ;
const interval = setInterval ( async ( ) => {
retries += 1 ;
const activeSessionId = window . sessionModule ? . getActiveSession ? . ( ) ? . id
|| document . body . dataset . activeSessionId
|| null ;
if ( activeSessionId ) {
await _ensureHeaderAffordances ( activeSessionId ) ;
}
if ( retries > 120 ) clearInterval ( interval ) ; // ~2 minutes
} , 1000 ) ;
}
// ── Boot ───────────────────────────────────────────────────────────────────
function _boot ( ) {
_watchForAssistantActivation ( ) ;
}
if ( document . readyState === 'loading' ) {
document . addEventListener ( 'DOMContentLoaded' , _boot ) ;
} else {
_boot ( ) ;
}
const assistantModule = {
openAssistantChat ,
openAssistantSettings ,
} ;
export default assistantModule ;