@@ -29,6 +29,7 @@ const coerceText = (value) => {
return fallback === '[object Object]' ? '' : fallback ;
} ;
const safeText = ( value ) => coerceText ( value ) . replace ( /[<>]/g , '' ) ;
const shortError = ( value ) => safeText ( value ) . split ( '\n' ) [ 0 ] . replace ( /\s+/g , ' ' ) . slice ( 0 , 140 ) ;
class JarvisProxy {
async connect ( ) {
@@ -66,29 +67,42 @@ class ArcOverlay {
if ( this . status . clutter _text ) this . status . clutter _text . ellipsize = Pango . EllipsizeMode . END ;
this . header . add _child ( this . title ) ; this . header . add _child ( new St . Widget ( { x _expand : true } ) ) ; this . header . add _child ( this . status ) ;
this . statusLine = new St . Label ( { text : 'Super+Shift+J · Hold Talk to speak' , style _class : 'jarvis-status-line' } ) ;
this . thinkingToggle = new St . Button ( { label : 'Thinking' , style _class : 'jarvis-thinking-toggle' , visible : false , can _focus : true , reactive : true } ) ;
this . thinkingToggle = new St . Button ( { label : 'Thinking' , style _class : 'jarvis-thinking-toggle' , visible : false , can _focus : true , reactive : true , x _align : Clutter . ActorAlign . START } ) ;
this . thinkingToggle . accessible _name = 'Show or hide Jarvis thinking' ;
this . thinkingToggle . connect ( 'clicked' , ( ) => this . toggleThinking ( ) ) ;
this . thinking = new St . Label ( { text : '' , style _class : 'jarvis-thinking' , visible : false , x _expand : true , can _focus : true } ) ;
this . thinking . accessible _name = 'Jarvis thinking' ;
if ( this . thinking . clutter _text ) { this . thinking . clutter _text . line _wrap = true ; this . thinking . clutter _text . ellipsize = Pango . EllipsizeMode . NONE ; }
this . notice = new St . Label ( { text : '' , style _class : 'jarvis-notice' , visible : false , x _expand : true } ) ;
this . notice . accessible _name = 'Jarvis notice' ;
this . job = new St . Label ( { text : '' , style _class : 'jarvis-job' , can _focus : true , visible : false } ) ; this . job . accessible _name = 'Jarvis job progress' ;
this . target = new St . Label ( { text : '' , style _class : 'jarvis-target' , can _focus : true , visible : false } ) ; this . target . accessible _name = 'Computer use target' ;
this . cursor = new St . Label ( { text : '' , style _class : 'jarvis-agent-cursor' , can _focus : false , visible : false } ) ; this . cursor . accessible _name = 'Visible computer use cursor' ;
this . scroll = new St . ScrollView ( { style _class : 'jarvis-transcript-scroll ' , overlay _scrollbars : true , x _expand : true , y _expand : true } ) ;
this . confirm = new St . BoxLayout ( { style _class : 'jarvis-confirm ' , visible : false , x _expand : true } ) ;
this . confirmLabel = new St . Label ( { text : '' , style _class : 'jarvis-confirm-label' , x _expand : true } ) ;
this . confirm . add _child ( this . confirmLabel ) ;
for ( const [ label , decision ] of [ [ 'Allow' , 'allow' ] , [ 'Deny' , 'deny' ] ] ) {
const button = new St . Button ( { label , style _class : 'jarvis-chip' , reactive : true , can _focus : true } ) ;
button . connect ( 'clicked' , ( ) => this . _answerConfirm ( decision ) ) ;
this . confirm . add _child ( button ) ;
}
this . scroll = new St . ScrollView ( { style _class : 'jarvis-transcript-scroll' , overlay _scrollbars : true , x _expand : true } ) ;
try { this . scroll . hscrollbar _policy = St . PolicyType . NEVER ; this . scroll . vscrollbar _policy = St . PolicyType . AUTOMATIC ; } catch { }
this . transcript = new St . BoxLayout ( { style _class : 'jarvis-transcript' , vertical : true , x _expand : true } ) ; this . transcript . accessible _name = 'Conversation transcript' ;
if ( typeof this . scroll . set _child === 'function' ) this . scroll . set _child ( this . transcript ) ; else this . scroll . add _child ( this . transcript ) ;
this . wave = new St . BoxLayout ( { style _class : 'jarvis-wave' , visible : false } ) ; this . bars = [ ] ;
for ( let i = 0 ; i < 48 ; i ++ ) { const bar = new St . Widget ( { style _class : 'jarvis-wave-bar' , height : 4 , y _align : Clutter . ActorAlign . CENTER } ) ; this . wave . add _child ( bar ) ; this . bars . push ( bar ) ; }
this . chipScroll = new St . ScrollView ( { style _class : 'jarvis-chip-scroll' , overlay _scrollbars : true , x _expand : true } ) ;
try { this . chipScroll . vscrollbar _policy = St . PolicyType . NEVER ; this . chipScroll . hscrollbar _policy = St . PolicyType . AUTOMATIC ; } catch { }
this . chips = new St . BoxLayout ( { style _class : 'jarvis-chips' } ) ; this . chips . accessible _name = 'Suggested actions' ;
if ( typeof this . chipScroll . set _child === 'function' ) this . chipScroll . set _child ( this . chips ) ; else this . chipScroll . add _child ( this . chips ) ;
for ( const mode of [ 'Chat' , 'Files' , 'Vision' , 'Imagine' , 'Compose' , 'Translate' , 'Dictate' , 'Computer' , 'Lab' ] ) {
const chip = new St . Button ( { label : mode , style _class : 'jarvis-chip' , can _focus : true , reactive : true } ) ; chip . accessible _name = ` ${ mode } mode ` ;
chip . connect ( 'clicked' , ( ) => this . onMode ? . ( mode . toLowerCase ( ) ) ) ; this . chips . add _child ( chip ) ;
}
this . root . add _child ( this . header ) ; this . root . add _child ( this . statusLine ) ; this . root . add _child ( this . thinkingToggle ) ; this . root . add _child ( this . thinking ) ; this . root . add _child ( this . job ) ; this . root . add _child ( this . target ) ; this . root . add _child ( this . cursor ) ; this . root . add _child ( this . wave ) ; this . root . add _child ( this . scroll ) ; this . root . add _child ( this . chips ) ;
this . controls = new St . BoxLayout ( { style _class : 'jarvis-chip s' } ) ;
this . talk = new St . Button ( { label : 'T alk' , style _class : 'jarvis-chip jarvis-talk' , reactive : true , can _focus : true } ) ;
this . root . add _child ( this . header ) ; this . root . add _child ( this . statusLine ) ; this . root . add _child ( this . thinkingToggle ) ; this . root . add _child ( this . thinking ) ; this . root . add _child ( this . notice ) ; this . root . add _child ( this . confirm ) ; this . root . add _child ( this . job ) ; this . root . add _child ( this . target ) ; this . root . add _child ( this . cursor ) ; this . root . add _child ( this . wave ) ; this . root . add _child ( this . scroll ) ; this . root . add _child ( this . chipScroll ) ;
this . controls = new St . BoxLayout ( { style _class : 'jarvis-control s' } ) ;
this . talk = new St . Button ( { label : 'Hold to t alk' , style _class : 'jarvis-chip jarvis-talk' , reactive : true , can _focus : true } ) ;
this . talk . accessible _name = 'Hold Space or Enter to talk' ;
this . talk . connect ( 'key-press-event' , ( _actor , event ) => { if ( [ Clutter . KEY _space , Clutter . KEY _Return ] . includes ( event . get _key _symbol ( ) ) ) { this . onTalk ? . ( true ) ; return Clutter . EVENT _STOP ; } return Clutter . EVENT _PROPAGATE ; } ) ;
this . talk . connect ( 'key-release-event' , ( _actor , event ) => { if ( [ Clutter . KEY _space , Clutter . KEY _Return ] . includes ( event . get _key _symbol ( ) ) ) { this . onTalk ? . ( false ) ; return Clutter . EVENT _STOP ; } return Clutter . EVENT _PROPAGATE ; } ) ;
@@ -98,7 +112,7 @@ class ArcOverlay {
this . talk . connect ( 'button-release-event' , ( ) => { this . onTalk ? . ( false ) ; return Clutter . EVENT _STOP ; } ) ;
this . controls . add _child ( this . talk ) ;
for ( const [ label , action ] of [ [ 'Stop' , ( ) => this . onStop ? . ( ) ] , [ 'Reset context' , ( ) => this . onReset ? . ( ) ] , [ 'Minimize' , ( ) => this . minimize ( ) ] , [ 'Close' , ( ) => this . hide ( ) ] ] ) {
const button = new St . Button ( { label , style _class : 'jarvis-chip' , reactive : true , can _focus : true } ) ;
const button = new St . Button ( { label , style _class : 'jarvis-chip jarvis-chip-quiet ' , reactive : true , can _focus : true } ) ;
button . connect ( 'clicked' , action ) ; this . controls . add _child ( button ) ;
}
this . entry = new St . Entry ( { hint _text : 'Ask Jarvis…' , can _focus : true , x _expand : true } ) ;
@@ -107,25 +121,31 @@ class ArcOverlay {
this . reducedMotion = false ;
this . streamingReply = false ;
this . replyFinalized = false ;
this . minimized = false ;
this . _state = 'ARMED' ;
}
attach ( ) { Main . layoutManager . addChrome ( this . root , { affectsStruts : false , trackFullscreen : false } ) ; this . halo = new St . Widget ( { style _class : 'jarvis-halo' , reactive : false } ) ; Main . layoutManager . addChrome ( this . halo , { affectsStruts : false , trackFullscreen : false } ) ; this . halo . hide ( ) ; this . hide ( ) ; }
show ( force = false ) {
if ( this . minimized && ! force ) return ;
this . minimized = false ;
const monitor = Main . layoutManager . primaryMonitor ;
if ( monitor ) {
const width = Math . min ( OVERLAY _WIDTH , monitor . width - 80 ) ;
this . root . set _width ( width ) ;
this . root . set _position ( monitor . x + Math . max ( 0 , Math . round ( ( monitor . width - width ) / 2 ) ) , monitor . y + 48 ) ;
this . scroll . set _height ( Math . max ( 100 , Math . min ( 280 , monitor . height - 360 ) ) ) ;
this . scroll . set _height ( Math . max ( 80 , Math . min ( 220 , monitor . height - 420 ) ) ) ;
}
const wasVisible = this . root . visible ;
this . root . visible = true ;
this . wave . visible = this . _state === 'LISTENING' || this . _state === 'SPEAKING' ;
if ( ! wasVisible ) this . entry . grab _key _focus ( ) ;
}
hide ( ) { this . onTalk ? . ( false ) ; this . root . visible = false ; this . minimized = false ; }
minimize ( ) { this . onTalk ? . ( false ) ; this . root . visible = false ; this . minimized = true ; }
_dismiss ( ) { this . onTalk ? . ( false ) ; this . root . visible = false ; this . minimized = true ; this . halo ? . hide ( ) ; }
hide ( ) { this . _dismiss ( ) ; }
minimize ( ) { this . _dismiss ( ) ; }
toggle ( ) { this . root . visible ? this . hide ( ) : this . show ( true ) ; }
clear ( ) { this . transcript . destroy _all _children ( ) ; this . thinking . text = '' ; this . thinking . visible = false ; this . thinkingToggle . visible = false ; this . streamingReply = false ; this . replyFinalized = false ; }
clear ( ) { this . transcript . destroy _all _children ( ) ; this . thinking . text = '' ; this . thinking . visible = false ; this . thinkingToggle . visible = false ; this . notice . visible = false ; this . confirm . visible = false ; this . streamingReply = false ; this . replyFinalized = false ; }
setNotice ( text ) { const body = shortError ( text ) ; this . notice . text = body ; this . notice . visible = Boolean ( body ) ; }
_followConversation ( ) {
const adjustment = this . scroll ? . get _vadjustment ? . ( ) || this . scroll ? . vadjustment ;
if ( ! adjustment ) return ;
@@ -134,22 +154,31 @@ class ArcOverlay {
return GLib . SOURCE _REMOVE ;
} ) ;
}
addRow ( who , text ) { const body = safeText ( text ) ; const row = new St . Label ( { text : ` ${ who === 'J' ? 'J' : 'YOU' } ${ body } ` , style _class : ` jarvis-row jarvis-row- ${ who === 'J' ? 'jarvis' : 'user' } ` , can _focus : true } ) ; row . accessible _name = ` ${ who === 'J' ? 'Jarvis' : 'You' } : ${ body } ` ; if ( row . clutter _text ) { row . clutter _text . line _wrap = true ; row . clutter _text . line _wrap _mode = Pango . WrapMode . WORD _CHAR ; row . clutter _text . ellipsize = Pango . EllipsizeMode . NONE ; } this . transcript . add _child ( row ) ; if ( this . transcript . get _n _children ( ) > 100 ) this . transcript . get _first _child ( ) . destroy ( ) ; this . show ( ) ; this . _followConversation ( ) ; return row ; }
token ( text ) { const chunk = safeText ( text ) ; if ( ! chunk ) return ; let row = this . transcript . get _last _child ? . ( ) ; if ( ! this . streamingReply || ! row || ! String ( row . style _class || '' ) . includes ( 'jarvis-row-jarvis' ) ) { row = this . addRow ( 'J' , '' ) ; this . streamingReply = true ; this . replyFinalized = false ; } row . text = ` ${ row . text } ${ chunk } ` ; row . accessible _name = ` Jarvis: ${ row . text } ` ; this . show ( ) ; this . _followConversation ( ) ; }
updateThinking ( text ) { const chunk = safeText ( text ) ; if ( ! chunk ) return ; this . thinking . text = ` ${ this . thinking . text || '' } ${ chunk } ` ; this . thinkingToggle . visible = true ; this . thinking . visible = true ; this . thinkingToggle . label = 'Thinking ▾' ; this . show ( ) ; }
addRow ( who , text ) { const body = safeText ( text ) ; const row = new St . Label ( { text : ` ${ who === 'J' ? 'J' : 'YOU' } ${ body } ` , style _class : ` jarvis-row jarvis-row- ${ who === 'J' ? 'jarvis' : 'user' } ` , can _focus : true } ) ; row . accessible _name = ` ${ who === 'J' ? 'Jarvis' : 'You' } : ${ body } ` ; if ( row . clutter _text ) { row . clutter _text . line _wrap = true ; row . clutter _text . line _wrap _mode = Pango . WrapMode . WORD _CHAR ; row . clutter _text . ellipsize = Pango . EllipsizeMode . NONE ; } this . transcript . add _child ( row ) ; if ( this . transcript . get _n _children ( ) > 100 ) this . transcript . get _first _child ( ) . destroy ( ) ; this . _followConversation ( ) ; return row ; }
token ( text ) { const chunk = safeText ( text ) ; if ( ! chunk ) return ; let row = this . transcript . get _last _child ? . ( ) ; if ( ! this . streamingReply || ! row || ! String ( row . style _class || '' ) . includes ( 'jarvis-row-jarvis' ) || String ( row . style _class || '' ) . includes ( 'jarvis-row-tool' ) ) { row = this . addRow ( 'J' , '' ) ; this . streamingReply = true ; this . replyFinalized = false ; } row . text = ` ${ row . text } ${ chunk } ` ; row . accessible _name = ` Jarvis: ${ row . text } ` ; this . _followConversation ( ) ; }
updateThinking ( text ) { const chunk = safeText ( text ) ; if ( ! chunk ) return ; this . thinking . text = ` ${ this . thinking . text || '' } ${ chunk } ` ; this . thinkingToggle . visible = true ; this . thinkingToggle . label = 'Thinking ▾' ; }
toggleThinking ( ) { this . thinking . visible = ! this . thinking . visible ; this . thinkingToggle . label = this . thinking . visible ? 'Thinking ▾' : 'Thinking ▸' ; }
finishThinking ( ) { if ( this . thinking . text ) { this . thinkingToggle . visible = true ; this . thinkingToggle . label = 'Thinking ▸' ; this . thinking . visible = false ; } }
addToolCall ( json ) { this . streamingReply = false ; try { const call = JSON . parse ( json ) ; this . addRow ( 'J' , ` Using ${ safeText ( call . name || 'tool' ) } … ` ) ; } catch { this . addRow ( 'J' , ` Using ${ safeText ( json ) } … ` ) ; } this . finishThinking ( ) ; }
addToolResult ( json ) { this . streamingReply = false ; try { const result = JSON . parse ( json ) ; this . addRow ( 'J' , ` ${ safeText ( result . name || 'tool' ) } complete ` ) ; } catch { this . addRow ( 'J' , 'Tool complete' ) ; } this . finishThinking ( ) ; }
addToolCall ( json ) { this . streamingReply = false ; this . replyFinalized = false ; try { const call = JSON . parse ( json ) ; this . _addToolRow ( ` Using ${ safeText ( call . name || 'tool' ) } … ` ) ; } catch { this . _addToolRow ( ` Using ${ safeText ( json ) } … ` ) ; } }
addToolResult ( json ) { this . streamingReply = false ; try { const result = JSON . parse ( json ) ; this . _addToolRow ( ` ${ safeText ( result . name || 'tool' ) } complete ` ) ; } catch { this . _addToolRow ( 'Tool complete' ) ; } }
_addToolRow ( text ) { const row = this . addRow ( 'J' , text ) ; row . style _class = ` ${ row . style _class } jarvis-row-tool ` ; }
offerConfirm ( tool , argsJson , pattern ) {
let jobId = '' ; let toolCallId = '' ;
try { const parsed = JSON . parse ( argsJson ) ; jobId = parsed . jobId || '' ; toolCallId = parsed . toolCallId || '' ; } catch { }
this . _confirmJob = jobId ; this . _confirmCall = toolCallId ;
this . confirmLabel . text = ` Allow ${ safeText ( tool ) } ? ${ safeText ( pattern ) } ` . trim ( ) ;
this . confirm . visible = true ;
}
_answerConfirm ( decision ) { this . confirm . visible = false ; this . onConfirm ? . ( this . _confirmJob || '' , this . _confirmCall || '' , decision ) ; }
finalizeReply ( text ) {
this . finishThinking ( ) ;
const spoken = safeText ( text ) ;
if ( this . replyFinalized ) return ;
let row = this . transcript . get _last _child ? . ( ) ;
if ( this . streamingReply && row && String ( row . style _class || '' ) . includes ( 'jarvis-row-jarvis' ) ) {
if ( this . streamingReply && row && String ( row . style _class || '' ) . includes ( 'jarvis-row-jarvis' ) && ! String ( row . style _class || '' ) . includes ( 'jarvis-row-tool' ) ) {
const body = String ( row . text || '' ) . replace ( /^J\s+/ , '' ) ;
if ( spoken && ! body . trim ( ) ) { row . text = ` J ${ spoken } ` ; row . accessible _name = ` Jarvis: ${ spoken } ` ; }
this . streamingReply = false ; this . replyFinalized = true ; this . show ( ) ; return ;
this . streamingReply = false ; this . replyFinalized = true ; return ;
}
if ( spoken ) this . addRow ( 'J' , spoken ) ;
this . replyFinalized = true ;
@@ -161,18 +190,19 @@ class ArcOverlay {
}
setState ( state ) {
const value = STATES . has ( state ) ? state : 'ARMED' ;
this . _state = value ;
this . statusLine . text = ` ${ value . toLowerCase ( ) } · Super+Shift+J · Hold Talk to speak ` ;
this . title . text = ` ${ value === 'SLEEPING' ? '⧸ ' : '◉' } JARVIS ` ;
this . status . style _class = ` jarvis-local jarvis-state- ${ value . toLowerCase ( ) } ` ;
this . wave . visible = value === 'LISTENING' || value === 'SPEAKING' ;
if ( value === 'LISTENING' ) this . showHalo ( ) ; if ( value !== 'ARMED' ) this . show ( ) ;
this . wave . visible = ! this . minimized && ( value === 'LISTENING' || value === 'SPEAKING' ) ;
if ( value === 'LISTENING' ) this . showHalo ( ) ;
}
level ( rms ) { this . wave . visible = true ; const value = Math . max ( 0 , Math . min ( 1 , Number ( rms ) || 0 ) ) ; for ( let i = 0 ; i < this . bars . length ; i ++ ) this . bars [ i ] . height = Math . max ( 4 , Math . round ( 4 + value * 32 * ( 0.45 + Math . abs ( Math . sin ( i * 1.7 ) ) * 0.55 ) ) ) ; }
level ( rms ) { if ( this . minimized ) return ; this . wave . visible = true ; const value = Math . max ( 0 , Math . min ( 1 , Number ( rms ) || 0 ) ) ; for ( let i = 0 ; i < this . bars . length ; i ++ ) this . bars [ i ] . height = Math . max ( 4 , Math . round ( 4 + value * 32 * ( 0.45 + Math . abs ( Math . sin ( i * 1.7 ) ) * 0.55 ) ) ) ; }
addChip ( id , label , payload ) { const chip = new St . Button ( { label : safeText ( label ) , style _class : 'jarvis-chip jarvis-chip-suggested' , can _focus : true } ) ; chip . accessible _name = ` Suggested action: ${ safeText ( label ) } ` ; chip . connect ( 'clicked' , ( ) => this . onSuggestion ? . ( id , payload ) ) ; this . chips . add _child ( chip ) ; }
addStep ( json ) { try { const step = JSON . parse ( json ) ; this . addRow ( 'J' , ` ${ step . n ? ` ${ step . n } . ` : '' } ${ step . action || 'computer step' } ` ) ; } catch { this . addRow ( 'J' , json ) ; } }
addJob ( id , pct , label ) { this . job . text = ` ${ safeText ( label ) } · ${ Math . round ( Number ( pct ) * 100 ) } % ` ; this . job . visible = true ; this . show ( ) ; }
setTarget ( json ) { this . target . text = ` ⌾ ${ safeText ( json ) } ` ; this . cursor . text = '◎' ; this . cursor . visible = true ; this . target . visible = true ; this . show ( ) ; }
showHalo ( ) { if ( ! this . halo ) return ; this . halo . show ( ) ; if ( this . _haloTimeout ) GLib . Source . remove ( this . _haloTimeout ) ; this . _haloTimeout = GLib . timeout _add ( GLib . PRIORITY _DEFAULT , 400 , ( ) => { this . _haloTimeout = 0 ; this . halo ? . hide ( ) ; return GLib . SOURCE _REMOVE ; } ) ; }
addJob ( id , pct , label ) { this . job . text = ` ${ safeText ( label ) } · ${ Math . round ( Number ( pct ) * 100 ) } % ` ; this . job . visible = true ; }
setTarget ( json ) { this . target . text = ` ⌾ ${ safeText ( json ) } ` ; this . cursor . text = '◎' ; this . cursor . visible = true ; this . target . visible = true ; }
showHalo ( ) { if ( ! this . halo || this . minimized || ! this . root . visible ) return ; this . halo . show ( ) ; if ( this . _haloTimeout ) GLib . Source . remove ( this . _haloTimeout ) ; this . _haloTimeout = GLib . timeout _add ( GLib . PRIORITY _DEFAULT , 400 , ( ) => { this . _haloTimeout = 0 ; this . halo ? . hide ( ) ; return GLib . SOURCE _REMOVE ; } ) ; }
destroy ( ) { if ( this . _haloTimeout ) GLib . Source . remove ( this . _haloTimeout ) ; this . _haloTimeout = 0 ; this . halo ? . destroy ( ) ; this . halo = null ; this . root . destroy ( ) ; }
}
@@ -186,10 +216,14 @@ export default class JarvisExtension extends Extension {
Main . panel . addToStatusArea ( 'jarvis-qvac' , this . _indicator , 0 , 'right' ) ;
this . _indicator . connect ( 'button-press-event' , ( _actor , event ) => { const button = event . get _button ( ) ; if ( button === 2 ) { this . _call ( 'Arm' ) ; return Clutter . EVENT _STOP ; } if ( button === 3 ) { this . _buildMenu ( ) ; return Clutter . EVENT _STOP ; } this . overlay . toggle ( ) ; return Clutter . EVENT _STOP ; } ) ;
this . overlay . onTalk = ( pressed ) => { if ( pressed ) { this . _call ( 'PushToTalk' , '(b)' , [ true ] ) ; this . _call ( 'Arm' ) ; } else { this . _call ( 'PushToTalk' , '(b)' , [ false ] ) ; } } ;
this . overlay . onStop = ( ) => this . _call ( 'Cancel' ) ; this . overlay . onReset = ( ) => this . _call ( 'ResetContext' ) ; this . overlay . onAsk = ( text ) => { this . overlay . addRow ( 'U' , text ) ; this . _call ( 'Ask' , '(s)' , [ text ] ) ; } ;
this . overlay . onMode = ( mode ) => this . _call ( 'SetMode' , '(s)' , [ mode ] ) ; this . overlay . onSuggestion = ( id , payload ) => this . _call ( 'Ask' , '(s)' , [ payload ? . text || payload ? . prompt || id ] ) ;
this . _keyName = 'hotkey' ; try { Main . wm . addKeybinding ( this . _keyName , this . settings , Meta . KeyBindingFlags . NONE , Shell . ActionMode . NORMAL | Shell . ActionMode . OVERVIEW , ( ) => this . overlay . toggle ( ) ) ; } catch ( error ) { log ( ` Jarvis hotkey unavailable: ${ error . message } ` ) ; }
this . _settingsChanged = this . settings . connect ( 'changed::accent-color' , ( ) => this . _applyAccent ( ) ) ; this . _applyAccent ( ) ; this . _connectDaemon ( ) ; this . overlay . show ( ) ;
this . overlay . onStop = ( ) => this . _call ( 'Cancel' ) ;
this . overlay . onReset = ( ) => { this . overlay . clear ( ) ; this . overlay . setNotice ( 'New conversation' ) ; this . _call ( 'ResetContext' ) ; } ;
this . overlay . onAsk = ( text ) => { this . overlay . addRow ( 'U' , text ) ; this . _call ( 'Ask' , '(s)' , [ text ] ) ; } ;
this . overlay . onMode = ( mode ) => this . _call ( 'SetMode' , '(s)' , [ mode ] ) ;
this . overlay . onSuggestion = ( id , payload ) => this . _call ( 'Ask' , '(s)' , [ payload ? . text || payload ? . prompt || id ] ) ;
this . overlay . onConfirm = ( jobId , toolCallId , decision ) => this . _call ( 'Confirm' , '(sss)' , [ jobId , toolCallId , decision ] ) ;
this . _keyName = 'hotkey' ; try { Main . wm . addKeybinding ( this . _keyName , this . settings , Meta . KeyBindingFlags . NONE , Shell . ActionMode . NORMAL | Shell . ActionMode . OVERVIEW , ( ) => this . overlay . show ( true ) ) ; } catch ( error ) { log ( ` Jarvis hotkey unavailable: ${ error . message } ` ) ; }
this . _settingsChanged = this . settings . connect ( 'changed::accent-color' , ( ) => this . _applyAccent ( ) ) ; this . _applyAccent ( ) ; this . _connectDaemon ( ) ; this . overlay . show ( true ) ;
try { this . _lockChanged = Main . screenShield . connect ( 'locked-changed' , ( ) => { if ( Main . screenShield . locked ) { this . overlay . hide ( ) ; this . _call ( 'ComputerRevoke' ) ; } } ) ; } catch { }
}
async _connectDaemon ( ) {
@@ -198,21 +232,22 @@ export default class JarvisExtension extends Extension {
await proxy . connect ( ) ; if ( this . proxy !== proxy ) return ;
this . _signals = [
[ 'StateChanged' , ( state ) => { this . _setState ( state ) ; this . _refreshVoiceStatus ( ) ; } ] ,
[ 'ContextReset' , ( ) => this . overlay . clear ( ) ] ,
[ 'WakeHeard' , ( ) => this . overlay . show ( ) ] ,
[ 'ContextReset' , ( ) => { this . overlay . clear ( ) ; this . overlay . setNotice ( 'New conversation' ) ; } ] ,
[ 'WakeHeard' , ( ) => { } ] ,
[ 'PartialTranscript' , ( text ) => this . overlay . addRow ( 'U' , text ) ] ,
[ 'Token' , ( text ) => this . overlay . token ( text ) ] ,
[ 'Thinking' , ( text ) => this . overlay . updateThinking ( text ) ] ,
[ 'ToolCall' , ( json ) => this . overlay . addToolCall ( json ) ] ,
[ 'ToolResult' , ( json ) => this . overlay . addToolResult ( json ) ] ,
[ 'Reply' , ( text ) => { this . overlay . finalizeReply ( text ) ; this . overlay . show ( ) ; } ] ,
[ 'Reply' , ( text ) => this . overlay . finalizeReply ( text ) ] ,
[ 'SpeakingLevel' , ( rms ) => this . overlay . level ( rms ) ] ,
[ 'ListeningLevel' , ( rms ) => this . overlay . level ( rms ) ] ,
[ 'ChipOffered' , ( id , label , payload ) => this . overlay . addChip ( id , label , payload ) ] ,
[ 'JobProgress' , ( id , pct , label ) => this . overlay . addJob ( id , pct , label ) ] ,
[ 'ComputerStep' , ( json ) => this . overlay . addStep ( json ) ] ,
[ 'ComputerHighlight' , ( json ) => this . overlay . setTarget ( json ) ] ,
[ 'Error ' , ( code , message ) => { this . overlay . addRow ( 'J' , message ) ; this . overlay . finishThinking ( ) ; if ( coerceText ( code ) === 'VOICE_UNAVAILABLE' ) this . overlay . setConnectionStatus ( 'voice-unavailable' ) ; } ] ,
[ 'ConfirmationRequired ' , ( tool , args , pattern ) => this . overlay . offerConfirm ( tool , args , pattern ) ] ,
[ 'Error' , ( code , message ) => { this . overlay . setNotice ( message ) ; this . overlay . finishThinking ( ) ; if ( coerceText ( code ) === 'VOICE_UNAVAILABLE' ) this . overlay . setConnectionStatus ( 'voice-unavailable' ) ; } ] ,
] . map ( ( [ name , handler ] ) => proxy . on ( name , handler ) ) ;
proxy . onOwnerChanged = ( ) => { if ( this . proxy !== proxy ) return ; this . _syncDaemon ( ) ; } ;
await this . _syncDaemon ( ) ;
@@ -244,10 +279,16 @@ export default class JarvisExtension extends Extension {
}
} catch { this . overlay ? . setConnectionStatus ( 'voice-unavailable' ) ; }
}
_call ( name , signature , value ) { this . proxy . call ( name , signature , value ) . catch ( ( error ) => { log ( ` Jarvis ${ name } : ${ error . message } ` ) ; this . overlay ? . addRow ( 'J' , ` ${ name } failed: ${ error . message } ` ) ; } ) ; }
_call ( name , signature , value ) {
this . proxy . call ( name , signature , value ) . catch ( ( error ) => {
log ( ` Jarvis ${ name } : ${ error . message } ` ) ;
const fallback = name === 'ResetContext' ? 'Could not reset conversation' : ` ${ name } failed: ${ shortError ( error . message ) } ` ;
this . overlay ? . setNotice ( fallback ) ;
} ) ;
}
_setState ( state ) { const value = safeText ( state ) ; this . _glyph . text = ( { ARMED : '◯' , LISTENING : '◌' , THINKING : '◉' , SPEAKING : '◎' , SLEEPING : '◐' } ) [ value ] || '◯' ; this . _glyph . text += ' Jarvis' ; this . _glyph . accessible _name = ` Jarvis ${ value . toLowerCase ( ) } ` ; this . overlay . setState ( value ) ; }
_applyAccent ( ) { this . overlay . root . set _style ( ` --jarvis-accent: ${ this . settings . get _string ( 'accent-color' ) } ; ` ) ; }
_applyAccessibility ( ) { try { const desktop = new Gio . Settings ( { schema _id : 'org.gnome.desktop.interface' } ) ; this . overlay . reducedMotion = desktop . list _keys ( ) . includes ( 'enable-animations' ) && ! desktop . get _boolean ( 'enable-animations' ) ; const theme = desktop . list _keys ( ) . includes ( 'gtk-theme' ) ? desktop . get _string ( 'gtk-theme' ) : '' ; if ( /high.?contrast/i . test ( theme ) ) this . overlay . root . add _style _class _name ( 'jarvis-high-contrast' ) ; } catch { } this . overlay . root . connect ( 'key-press-event' , ( _actor , event ) => { if ( event . get _key _symbol ( ) === Clutter . KEY _Escape ) { this . _call ( 'Cancel' ) ; this . overlay . hide ( ) ; return Clutter . EVENT _STOP ; } return Clutter . EVENT _PROPAGATE ; } ) ; }
_buildMenu ( ) { const menu = this . _indicator . menu ; menu . removeAll ( ) ; for ( const [ label , action ] of [ [ 'Open ARC' , ( ) => this . overlay . show ( ) ] , [ 'Talk' , ( ) => this . _call ( 'Arm' ) ] , [ 'Stop' , ( ) => this . _call ( 'Cancel' ) ] , [ 'Reset context' , ( ) => this . _call ( 'ResetContext' ) ] , [ 'Privacy mode' , ( ) => this . _call ( 'Sleep' ) ] , [ 'Settings' , ( ) => this . openPreferences ( ) ] ] ) { const item = new PopupMenu . PopupMenuItem ( label ) ; item . connect ( 'activate' , action ) ; menu . addMenuItem ( item ) ; } menu . open ( ) ; }
_buildMenu ( ) { const menu = this . _indicator . menu ; menu . removeAll ( ) ; for ( const [ label , action ] of [ [ 'Open ARC' , ( ) => this . overlay . show ( true ) ] , [ 'Talk' , ( ) => this . _call ( 'Arm' ) ] , [ 'Stop' , ( ) => this . _call ( 'Cancel' ) ] , [ 'Reset context' , ( ) => this . _call ( 'ResetContext' ) ] , [ 'Privacy mode' , ( ) => this . _call ( 'Sleep' ) ] , [ 'Settings' , ( ) => this . openPreferences ( ) ] ] ) { const item = new PopupMenu . PopupMenuItem ( label ) ; item . connect ( 'activate' , action ) ; menu . addMenuItem ( item ) ; } menu . open ( ) ; }
disable ( ) { this . _removeShellService ? . ( ) ; try { Main . screenShield . disconnect ( this . _lockChanged ) ; } catch { } try { Main . wm . removeKeybinding ( this . _keyName ) ; } catch { } if ( this . _settingsChanged ) this . settings . disconnect ( this . _settingsChanged ) ; this . _signals ? . forEach ( ( id ) => this . proxy ? . proxy ? . disconnect ( id ) ) ; this . proxy ? . close ( ) ; this . overlay ? . destroy ( ) ; this . _indicator ? . destroy ( ) ; if ( this . _theme && this . _stylesheet ) { try { this . _theme . unload _stylesheet ( this . _stylesheet ) ; } catch { } } this . overlay = this . _indicator = this . _glyph = this . proxy = null ; }
}