@shardflux/desktop-linux-x64 0.1.0-next.4 → 0.1.0-next.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/app/manifest.json +1 -1
- package/app/shardflux-desktop +0 -0
- package/app/ui/icons.mjs +3 -0
- package/app/ui/integrations.mjs +2 -2
- package/app/ui/settings.mjs +18 -7
- package/app/ui/style.css +70 -0
- package/app/ui/surfaces.mjs +4 -2
- package/app/ui/tray.mjs +1 -1
- package/package.json +1 -1
package/app/manifest.json
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"id":"dev.shardflux.Desktop","version":"0.1.0-next.
|
|
1
|
+
{"id":"dev.shardflux.Desktop","version":"0.1.0-next.5"}
|
package/app/shardflux-desktop
CHANGED
|
Binary file
|
package/app/ui/icons.mjs
CHANGED
|
@@ -33,6 +33,9 @@ const paths={
|
|
|
33
33
|
link:'<path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"/><path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"/>',
|
|
34
34
|
'git-branch':'<line x1="6" x2="6" y1="3" y2="15"/><circle cx="18" cy="6" r="3"/><circle cx="6" cy="18" r="3"/><path d="M18 9a9 9 0 0 1-9 9"/>', // arrow.triangle.branch
|
|
35
35
|
bell:'<path d="M10.268 21a2 2 0 0 0 3.464 0"/><path d="M3.262 15.326A1 1 0 0 0 4 17h16a1 1 0 0 0 .74-1.673C19.41 13.956 18 12.499 18 8A6 6 0 0 0 6 8c0 4.499-1.411 5.956-2.738 7.326"/>',
|
|
36
|
+
'circle-user':'<circle cx="12" cy="12" r="10"/><circle cx="12" cy="10" r="3"/><path d="M7 20.662V19a2 2 0 0 1 2-2h6a2 2 0 0 1 2 2v1.662"/>', // person.crop.circle
|
|
37
|
+
'messages-square':'<path d="M14 9a2 2 0 0 1-2 2H6l-4 4V4a2 2 0 0 1 2-2h8a2 2 0 0 1 2 2z"/><path d="M18 9h2a2 2 0 0 1 2 2v11l-4-4h-6a2 2 0 0 1-2-2v-1"/>', // bubble.left.and.bubble.right
|
|
38
|
+
command:'<path d="M15 6v12a3 3 0 1 0 3-3H6a3 3 0 1 0 3 3V6a3 3 0 1 0-3 3h12a3 3 0 1 0-3-3"/>',
|
|
36
39
|
user:'<path d="M19 21v-2a4 4 0 0 0-4-4H9a4 4 0 0 0-4 4v2"/><circle cx="12" cy="7" r="4"/>',
|
|
37
40
|
'circle-arrow-down':'<circle cx="12" cy="12" r="10"/><path d="M12 8v8"/><path d="m8 12 4 4 4-4"/>', // arrow.down.circle
|
|
38
41
|
'rotate-cw':'<path d="M21 12a9 9 0 1 1-9-9c2.52 0 4.93 1 6.74 2.74L21 8"/><path d="M21 3v5h-5"/>', // arrow.clockwise
|
package/app/ui/integrations.mjs
CHANGED
|
@@ -13,8 +13,8 @@ export function installIntegrations({model,host,cli,render,button,registerAction
|
|
|
13
13
|
function request(){return I.requests.find(r=>r.request_id===model.prefs.reviewDevice)||I.requests[0];}
|
|
14
14
|
function credential(){return I.credentials.find(r=>r.id===model.prefs.reviewCredential)||I.credentials[0];}
|
|
15
15
|
const path=(label,value)=>value?`<div class="permission-path"><small>${label}</small><code>${e(value)}</code></div>`:'';
|
|
16
|
-
function panel(){const r=request();if(!r)return `<div class="empty">This request is no longer waiting for approval.</div>`;return `<div class="permission"><header class="permission-heading"><span class="permission-icon">${icon(r.kind==='exec'?'monitor':'folder',24)}</span><div><h2>${r.outside?'Allow access outside this conversation?':({exec:'Run a command on your computer?',files_pull:'Share files from your computer?',files_push:'Save files to your computer?',try_locally:'Try this conversation on your computer?',port_forward:'Open a local preview?',port_reverse:'Connect to a service on your computer?'})[r.kind]||'Allow this conversation on your computer?'}</h2><p>${e([r.conversation_title,r.project_name].filter(Boolean).join(' · '))}</p></div></header><div class="permission-details">${r.argv?.length?`<pre>${r.argv.map(a=>e(/^[\w/=:.,@%+-]+$/.test(a)?a:JSON.stringify(a))).join(' ')}</pre>`:`<p>${e(r.summary)}</p>`}${path('Folder',r.local_path)}${path('In the conversation',r.remote_path)}${r.local_port?path('On your computer',`localhost:${r.local_port}`)+path('In the conversation',`Port ${r.remote_port}`):''}</div>${r.outside?'<p class="error">This folder is outside the conversation’s folder. Approval applies to this request only.</p>':`<label class="permission-always"><input type="checkbox" name="always-device" ${I.always?'checked':''}> <span>Allow these requests for this project<small>Applies to this type of request in this project.</small></span></label>`}<footer class="row spread">${button('Deny','deny-device')}${button(I.always&&!r.outside?'Allow for project':'Allow once','allow-device','primary')}</footer>${model.error?`<p class="error">${e(model.error)}</p>`:''}</div>`;}
|
|
17
|
-
function credentialPanel(){const r=credential();if(!r)return '<div class="empty">This sign-in request is no longer waiting.</div>';return `<div class="permission"><header class="permission-heading"><span class="permission-icon">${icon('user',24)}</span><div><h2>Sign in to ${e(new URL(r.site).host)}</h2><p>Your conversation is asking for a login.</p></div></header><p>Share ${r.fields.map(e).join(' and ')} from your vault.</p><div class="permission-details"><label>Vault
|
|
16
|
+
function panel(){const r=request();if(!r)return `<div class="empty">This request is no longer waiting for approval.</div>`;return `<div class="permission"><header class="permission-heading"><span class="permission-icon">${icon(r.kind==='exec'?'monitor':'folder',24)}</span><div><h2>${r.outside?'Allow access outside this conversation?':({exec:'Run a command on your computer?',files_pull:'Share files from your computer?',files_push:'Save files to your computer?',try_locally:'Try this conversation on your computer?',port_forward:'Open a local preview?',port_reverse:'Connect to a service on your computer?'})[r.kind]||'Allow this conversation on your computer?'}</h2><p>${e([r.conversation_title,r.project_name].filter(Boolean).join(' · '))}</p></div></header><div class="permission-details">${r.argv?.length?`<pre>${r.argv.map(a=>e(/^[\w/=:.,@%+-]+$/.test(a)?a:JSON.stringify(a))).join(' ')}</pre>`:`<p>${e(r.summary)}</p>`}${path('Folder',r.local_path)}${path('In the conversation',r.remote_path)}${r.local_port?path('On your computer',`localhost:${r.local_port}`)+path('In the conversation',`Port ${r.remote_port}`):''}</div>${r.outside?'<p class="error">This folder is outside the conversation’s folder. Approval applies to this request only.</p>':`<label class="permission-always"><input type="checkbox" name="always-device" ${I.always?'checked':''}> <span>Allow these requests for this project<small>Applies to this type of request in this project.</small></span></label>`}<footer class="row spread">${button('Deny','deny-device','plain-text')}${button(I.always&&!r.outside?'Allow for project':'Allow once','allow-device','primary')}</footer>${model.error?`<p class="error">${e(model.error)}</p>`:''}</div>`;}
|
|
17
|
+
function credentialPanel(){const r=credential();if(!r)return '<div class="empty">This sign-in request is no longer waiting.</div>';return `<div class="permission"><header class="permission-heading"><span class="permission-icon">${icon('user',24)}</span><div><h2>Sign in to ${e(new URL(r.site).host)}</h2><p>Your conversation is asking for a login.</p></div></header><p>Share ${r.fields.map(e).join(' and ')} from your vault.</p><div class="permission-details inline"><label class="inline-row"><span>Vault</span><select name="vault"><option value="1password" ${I.vault==='1password'?'selected':''}>1Password</option><option value="bitwarden" ${I.vault==='bitwarden'?'selected':''}>Bitwarden</option><option value="hsec" ${I.vault==='hsec'?'selected':''}>hsec</option></select></label><label class="inline-row"><span>Login item</span><input name="vault-item" value="${e(I.item)}" placeholder="GitHub personal account"></label></div><label class="inline-row scope"><span>Future requests</span><select name="credential-scope">${[['ask','Ask every time'],['project','Allow for this project'],['machine','Allow on this machine']].map(([v,t])=>`<option value="${v}" ${I.scope===v?'selected':''}>${t}</option>`).join('')}</select></label><p>Only these fields on ${e(new URL(r.site).host)}.</p><label class="permission-always"><input type="checkbox" name="credential-remember" ${I.remember?'checked':''}><span>Remember for unattended sign-in<small>Save an encrypted copy for ${I.scope==='ask'?'this conversation':I.scope==='project'?'this project’s template':'your machine template'}.</small></span></label><footer class="row spread">${button('Deny','deny-credential','plain-text')}${button('Allow login','allow-credential','primary',!I.item.trim()||!!r.expires_at&&Date.parse(r.expires_at)<Date.now())}</footer>${model.error?`<p class="error">${e(model.error)}</p>`:''}</div>`;}
|
|
18
18
|
function files(){return `<section class="files-window"><h2>Shardflux files</h2><p>Your conversations’ committed files, together.</p><div class="row">${button('Open files','open-files','primary')}<code>${e(I.files.root)}</code></div>${I.files.placeholders.length?`<h3>Ready to download</h3><div class="setting-group">${I.files.placeholders.map((f,i)=>`<div class="setting-row"><div><b>${e(f.path.split('/').at(-1))}</b><p>${e(f.title||f.conversation)} · ${(f.size/1e6).toFixed(1)} MB</p></div>${button('Download',`download:${i}`,'small')}</div>`).join('')}</div>`:'<p>Files appear here as your conversations save them.</p>'}${model.error?`<p class="error">${e(model.error)}</p>`:''}</section>`;}
|
|
19
19
|
window.renderSurface=()=>{if(['approvals','credential','files'].includes(model.view)){document.querySelector('#app').innerHTML=model.view==='approvals'?panel():model.view==='credential'?credentialPanel():files();if(model.view!=='files')requestAnimationFrame(()=>void host('resize',{height:document.querySelector('.permission')?.scrollHeight||320}));}else prior();};
|
|
20
20
|
window.sfRefreshIntegrations=()=>refresh();
|
package/app/ui/settings.mjs
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import {icon,appMark,spinner} from './icons.mjs';
|
|
2
|
-
import {escape as e,cardState} from './core.mjs';
|
|
2
|
+
import {escape as e,cardState,shortcutLabel} from './core.mjs';
|
|
3
3
|
const panes=['Account','General','Agents','Launcher','Notifications','Updates','About'];
|
|
4
|
+
// SettingsPane.icon and .tint: SF Symbols as their Lucide counterparts, each on a 13% tile of its tint.
|
|
5
|
+
const paneIcons={Account:['circle-user','var(--sf-glass-glow-cold)'],General:['settings','var(--sf-muted)'],Agents:['messages-square','var(--sf-molten)'],Launcher:['command','var(--sf-ink)'],Notifications:['bell','var(--sf-warning)'],Updates:['circle-arrow-down','var(--sf-success)'],About:['info','var(--sf-dim)']};
|
|
4
6
|
const descriptions={Account:'Your account and this month’s activity.',General:'Make Shardflux feel at home on your computer.',Agents:'Your conversations, in the apps you already use.',Launcher:'Start a conversation without leaving your work.',Notifications:'Hear from an agent when it needs you.',Updates:'Keep your Shardflux apps up to date.',About:'A place for your agents to work.'};
|
|
5
7
|
export function installSettings({model,host,cli,render,button,registerActions,refresh,savePrefs}) {
|
|
6
8
|
const S={pane:'General',info:{},identity:null,usage:null,spend:null,org:null,profiles:[],update:null,recording:false,confirmSignout:false};
|
|
@@ -14,17 +16,26 @@ export function installSettings({model,host,cli,render,button,registerActions,re
|
|
|
14
16
|
case 'Account':{const cpu=S.usage?.allowances?.find(a=>a.key==='cpu_hours');const spend=S.spend?.estimated_period_total_minor;return group(`<div class="account-heading"><img src="assets/shardflux-tray.svg" alt=""><div><h2>${e(S.identity?.display_name||S.identity?.email||'Welcome to Shardflux')}</h2><p>${e(S.identity?.email||'Sign in to get started')}</p></div></div>${S.org?`<p class="account-plan">${e(S.usage?.plan?.plan_key||'')} · ${e(S.org.name)}</p>`:''}`)+group(row('This month','CPU time used by your conversations.',cpu?`<span class="mono">${cpu.used.toFixed(1)}${cpu.included!==undefined?' / '+cpu.included:''} CPU-h</span>`:'Usage unavailable')+row('Estimated spend','Subscription and usage charges this period.',typeof spend==='number'?new Intl.NumberFormat(undefined,{style:'currency',currency:S.spend.currency}).format(spend/100):'Spend unavailable'))+`<div class="row spread">${link('Open dashboard','https://app.shardflux.dev')}${button('Sign out…','signout','link')}</div>${S.confirmSignout?group(`<p>Sign out of Shardflux on this computer? Your conversations keep running. Saved project keys remain available.</p><div class="row">${button('Cancel','cancel-signout')}${button('Sign out','confirm-signout')}</div>`):''}`;}
|
|
15
17
|
case 'General':return group(row('Launch at login','Available when you sign in.',toggle('Launch at login','autostart',model.prefs.autostart!==false))+row('Profile','Used for new conversations.',`<select data-profile aria-label="Profile">${[...new Set([model.api,...S.profiles])].filter(Boolean).map(url=>`<option value="${e(url)}" ${url===model.api?'selected':''}>${url==='https://api.shardflux.dev'?'Production':e(new URL(url).host)}</option>`).join('')}</select>`))+group(row('Onboarding','Start, step away, reply anywhere, land.',button('Show onboarding','onboarding','small'))+row('Command center','Workspaces, files and account access.',button('Open','command-center','small'))+row('Archived conversations','Conversations hidden from this menu.',button('Restore','restore-archived','small')));
|
|
16
18
|
case 'Agents':return agents();
|
|
17
|
-
case 'Launcher':return group(
|
|
19
|
+
case 'Launcher':return group(`<div class="shortcut-recorder"><b>Open Shardflux from any app</b><button class="keycap ${S.recording?'recording':''}" data-action="record-shortcut" aria-label="Change launcher shortcut">${e(S.recording?'Press shortcut…':shortcutLabel(model.prefs.shortcut).replace(/\+/g,' '))}</button><p>${S.recording?'Press your shortcut. Esc cancels.':'Click the keys to record another shortcut.'}</p>${S.shortcutNotice?`<p class="notice">${icon('circle-alert',13)}<span>${e(S.shortcutNotice)}</span></p>`:''}</div>`)+group(row('Detect my project','Choose the last project automatically.',toggle('Detect project','detectProject',model.prefs.detectProject!==false))+row('Position','Drag the launcher by its background to move it. It reopens there.',button('Reset launcher position','reset-position','small')))+`<p class="setting-note">Other app shortcuts stay available. Shardflux checks for conflicts before saving.</p>`;
|
|
18
20
|
case 'Notifications':return group(row('Agent needs you','When an agent is waiting for a reply.',toggle('Agent needs you','notifyWaiting',model.prefs.notifyWaiting!==false))+row('Agent finished','When an agent finishes its work.',toggle('Agent finished','notifyDone',model.prefs.notifyDone!==false))+row('Approvals and sign-in','Permission and sign-in requests.',toggle('Approvals and sign-in','notifyApprovals',model.prefs.notifyApprovals!==false)))+group(row('Desktop notifications','Manage Shardflux notifications in GNOME.',button('Open Settings','notification-settings','small')));
|
|
19
|
-
case 'Updates':
|
|
21
|
+
case 'Updates':{const failed=S.lastUpdate?.ok===false,checking=S.checking,updating=model.updating;
|
|
22
|
+
const notice=checking||updating?`${spinner(14)}<span>${checking?'Checking for updates…':'Updating Shardflux…'}</span>`:failed?`${icon('circle-alert',15)}<span class="warning">${e(S.lastUpdate.message||'The update didn’t finish. Try again.')}</span>`:S.update?.update_available?`${icon('circle-arrow-down',15)}<span>An update is ready to install.</span>`:`${icon('circle-arrow-down',15)}<span class="${S.update?'success':'secondary'}">${S.update?'You’re up to date.':'Check for the latest Shardflux release.'}</span>`;
|
|
23
|
+
const title=updating?'Updating…':failed?'Try again':S.update?.update_available?'Update now':'Check for updates';
|
|
24
|
+
return group(row('Desktop app','Conversations from your top bar.',`<span class="mono">${e(S.info.version||'—')}</span>`)+(S.update?.components||[]).filter(c=>!/desktop-linux|menubar-darwin/.test(c.package||'')).map(c=>row(c.package==='@shardflux/cli'?'shard':c.label||c.package,c.package==='@shardflux/cli'?'Your command center and agent launcher.':'Installed with your Shardflux tools.',`<span class="mono">${e(c.current)}</span>`)).join(''))
|
|
25
|
+
+`<div class="update-block"><p class="update-notice">${notice}</p><div class="row spread">${button(title,S.update?.update_available&&!failed?'update-now':'check-updates','primary regular',checking||updating)}${S.checkedAt?`<small>Checked ${e(new Date(S.checkedAt).toLocaleTimeString([],{hour:'2-digit',minute:'2-digit'}))}</small>`:''}</div></div>`;}
|
|
20
26
|
case 'About':return `<div class="account-heading"><img src="assets/shardflux-tray.svg" alt=""><div><h2>Shardflux</h2><p>Version ${e(S.info.version||'Reading version…')}</p></div></div>`+group(row('Documentation','Guides for conversations, workspaces and the CLI.',link('Open','https://docs.shardflux.dev'))+row('Service status','Check the current service response.',link('Open','https://api.shardflux.dev/healthz'))+row('Support','Talk to the people building Shardflux.',link('Email','mailto:shardflux@heliosone.fi')));
|
|
21
27
|
}}
|
|
22
|
-
const
|
|
23
|
-
|
|
28
|
+
const accountName=()=>S.identity?.display_name||S.identity?.email||'Welcome to Shardflux';
|
|
29
|
+
const accountLine=()=>[S.usage?.plan?.plan_key&&S.usage.plan.plan_key[0].toUpperCase()+S.usage.plan.plan_key.slice(1),S.org?.name].filter(Boolean).join(' · ');
|
|
30
|
+
function sidebar(){
|
|
31
|
+
return `<aside><button class="sidebar-account" data-action="pane:Account"><img src="assets/shardflux-mark.svg" alt=""><span><b>${e(accountName())}</b><small>${e(accountLine()||(S.identity?'Personal':'Sign in to get started'))}</small></span></button><nav>${panes.map(p=>`${p==='Agents'||p==='Updates'?'<div class="nav-rule"></div>':''}<button class="${p===S.pane?'selected':''}" data-action="pane:${p}" aria-current="${p===S.pane?'page':'false'}"><span class="pane-icon" style="--tint:${paneIcons[p][1]}">${icon(paneIcons[p][0],14)}</span>${e(p)}</button>`).join('')}</nav></aside>`;
|
|
32
|
+
}
|
|
33
|
+
const prior=window.renderSurface;window.renderSurface=()=>{if(model.view!=='settings')return prior();document.querySelector('#app').innerHTML=`<div class="settings">${sidebar()}<section class="settings-content"><header><h2>${S.pane}</h2><p class="pane-description">${descriptions[S.pane]}</p></header>${body()}${model.error?`<p class="settings-error">${icon('circle-alert',13)}<span>${e(model.error)}</span></p>`:''}</section></div>`;};
|
|
34
|
+
async function load(){const api=model.api;const results=await Promise.allSettled([host('info'),host('profiles'),cli(['account','status','--json']),cli(['orgs','list','--json']),cli(['usage','--json']),cli(['usage','--estimate','--json']),cli(['agents','targets','--json']),cli(['menubar','status','--json'])]);if(api!==model.api)return;const value=i=>results[i].status==='fulfilled'?results[i].value:null;S.info=value(0)||{};if(value(7))model.prefs.autostart=value(7).login==='on';S.profiles=value(1)||[];S.identity=value(2)?.user;S.org=value(3)?.data?.[0];S.usage=value(4);S.spend=value(5);try{S.lastUpdate=await host('read',{name:'last-update.json'});}catch{}if(model.prefs.settingsPane&&panes.includes(model.prefs.settingsPane)){S.pane=model.prefs.settingsPane;delete model.prefs.settingsPane;await savePrefs();}if(!S.update&&model.settings?.update)S.checkedAt=S.checkedAt||Date.now();if(model.launcherState)model.launcherState.targets=value(6)||{};await refresh();render();}
|
|
24
35
|
window.addEventListener('sf-view',ev=>{if(ev.detail==='settings')void load();});
|
|
25
36
|
document.querySelector('#app').addEventListener('change',async ev=>{try{if(ev.target.dataset.pref){const key=ev.target.dataset.pref;model.prefs[key]=ev.target.checked;await savePrefs();if(key==='autostart')await cli(['menubar','status','--login',ev.target.checked?'on':'off','--json']);}if(ev.target.dataset.openIn){await cli(['agents','targets','--open-in',`${ev.target.dataset.openIn}=${ev.target.value}`,'--json']);await load();}if(ev.target.hasAttribute('data-profile'))await host('profile',{url:ev.target.value});}catch(err){model.error=err.message;render();}});
|
|
26
37
|
document.querySelector('#app').addEventListener('click',ev=>{const a=ev.target.closest('[data-action]')?.dataset.action;if(a?.startsWith('pane:')){S.pane=a.slice(5);S.recording=false;model.error='';render();}});
|
|
27
|
-
document.addEventListener('keydown',async ev=>{if(!S.recording)return;ev.preventDefault();if(ev.key==='Escape'){S.recording=false;render();return;}if(['Alt','Control','Shift','Meta'].includes(ev.key))return;const key=ev.code==='Space'?'space':ev.key.toLowerCase(),shortcut=(ev.ctrlKey?'<Control>':'')+(ev.altKey?'<Alt>':'')+(ev.shiftKey?'<Shift>':'')+(ev.metaKey?'<Super>':'')+key;try{await host('shortcut',{accelerator:shortcut});model.prefs.shortcut=shortcut;await savePrefs();S.recording=false;}catch(err){
|
|
28
|
-
registerActions({'record-shortcut':()=>{S.recording=true;},'reset-position':()=>host('reset-position'),'command-center':()=>host('terminal',{argv:['shard','agents','menu']}),'notification-settings':()=>host('desktop-settings',{panel:'notifications'}),'restore-archived':async()=>{model.prefs.archived={};await savePrefs();},signout:()=>S.confirmSignout=true,'cancel-signout':()=>S.confirmSignout=false,'confirm-signout':async()=>{await cli(['auth','logout','--json']);S.identity=null;S.confirmSignout=false;await host('view',{name:'onboarding'});},'check-updates':async()=>{S.update=await cli(['update','--check','--json']);},'update-now':async()=>{model.updating=true;try{S.update=await cli(['update','--json'],{timeout:540});}finally{model.updating=false;}},'t3-project':async()=>{const path=await host('choose',{kind:'folder'});if(path){const d=await cli(['agents','new','--in','t3','--path',path,'--json']);model.phone=d.t3_link;await refresh();}}});
|
|
38
|
+
document.addEventListener('keydown',async ev=>{if(!S.recording)return;ev.preventDefault();if(ev.key==='Escape'){S.recording=false;render();return;}if(['Alt','Control','Shift','Meta'].includes(ev.key))return;const key=ev.code==='Space'?'space':ev.key.toLowerCase(),shortcut=(ev.ctrlKey?'<Control>':'')+(ev.altKey?'<Alt>':'')+(ev.shiftKey?'<Shift>':'')+(ev.metaKey?'<Super>':'')+key;try{await host('shortcut',{accelerator:shortcut});model.prefs.shortcut=shortcut;await savePrefs();S.recording=false;S.shortcutNotice='';}catch(err){S.shortcutNotice=err.message;}render();},true);
|
|
39
|
+
registerActions({'record-shortcut':()=>{S.recording=true;},'reset-position':()=>host('reset-position'),'command-center':()=>host('terminal',{argv:['shard','agents','menu']}),'notification-settings':()=>host('desktop-settings',{panel:'notifications'}),'restore-archived':async()=>{model.prefs.archived={};await savePrefs();},signout:()=>S.confirmSignout=true,'cancel-signout':()=>S.confirmSignout=false,'confirm-signout':async()=>{await cli(['auth','logout','--json']);S.identity=null;S.confirmSignout=false;await host('view',{name:'onboarding'});},'check-updates':async()=>{S.checking=true;render();try{S.update=await cli(['update','--check','--json']);S.checkedAt=Date.now();}finally{S.checking=false;}},'update-now':async()=>{model.updating=true;try{S.update=await cli(['update','--json'],{timeout:540});}finally{model.updating=false;}},'t3-project':async()=>{const path=await host('choose',{kind:'folder'});if(path){const d=await cli(['agents','new','--in','t3','--path',path,'--json']);model.phone=d.t3_link;await refresh();}}});
|
|
29
40
|
if(model.view==='settings')void load();
|
|
30
41
|
}
|
package/app/ui/style.css
CHANGED
|
@@ -210,3 +210,73 @@ body:has(.launcher),body:has(.menu-panel){background:transparent;overflow:hidden
|
|
|
210
210
|
.settings .connector,.settings .connector.connected{box-shadow:none;border-color:transparent}
|
|
211
211
|
.menu-panel .session-title,.menu-panel .new-conversation,.menu-item{font-weight:450}
|
|
212
212
|
.menu-error .bordered{margin-top:6px;height:22px;font-size:12.5px}
|
|
213
|
+
.launcher button.control:disabled,.launcher .start:disabled{opacity:1}
|
|
214
|
+
.launcher .control.add:disabled{opacity:.45}
|
|
215
|
+
.launcher .control.limit:disabled{opacity:.45}
|
|
216
|
+
.result-row .row-check .ui-icon{stroke-width:2.4}
|
|
217
|
+
/* macOS draws Funnel Sans a touch heavier than WebKitGTK's FreeType; these weights match the Mac renders. */
|
|
218
|
+
.prompt-row textarea{font-weight:500;line-height:21px;min-height:21px;height:21px}
|
|
219
|
+
.prompt-row .lead,.prompt-row .progress{height:21px}
|
|
220
|
+
.prompt-row .add{margin-top:-3px}
|
|
221
|
+
.launcher .control,.launcher .start{font-weight:500}
|
|
222
|
+
.launcher .start{font-weight:600}
|
|
223
|
+
.launcher .project{font-weight:450}
|
|
224
|
+
/* AppSettingsView: a 200 px sidebar (the account, then the panes on tinted tiles, grouped), 24 px content with a
|
|
225
|
+
26 px title, groups of 12 px rows on a 3.5% ink fill. */
|
|
226
|
+
.settings{grid-template-columns:200px 1fr;background:var(--sf-bg)}
|
|
227
|
+
.settings aside{padding:16px 8px;background:#0e0e10;border-right:1px solid var(--sf-line);display:flex;flex-direction:column;gap:16px}
|
|
228
|
+
.sidebar-account{display:flex;align-items:center;gap:10px;border:0;border-radius:6px;padding:0 4px!important;text-align:left;background:transparent!important}
|
|
229
|
+
.sidebar-account img{width:36px;height:36px;filter:invert(1)}
|
|
230
|
+
.sidebar-account span{display:flex;flex-direction:column;gap:3px;min-width:0}
|
|
231
|
+
.sidebar-account b{font-size:13px;font-weight:500;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
|
|
232
|
+
.sidebar-account small{font-size:11px;color:var(--glass-secondary);overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
|
|
233
|
+
.settings nav{gap:4px}
|
|
234
|
+
.settings nav button{display:flex;align-items:center;gap:10px;height:36px;padding:0 8px;border:0;border-radius:6px;font-size:13px;color:var(--sf-ink)}
|
|
235
|
+
.settings nav button:hover{background:rgba(237,235,231,.05)}
|
|
236
|
+
.settings nav button.selected{background:rgba(237,235,231,.09);color:var(--sf-ink)}
|
|
237
|
+
.pane-icon{width:24px;height:24px;border-radius:6px;display:inline-flex;align-items:center;justify-content:center;color:var(--tint);background:color-mix(in srgb,var(--tint) 13%,transparent)}
|
|
238
|
+
.pane-icon .ui-icon{stroke-width:1.9}
|
|
239
|
+
.nav-rule{height:1px;background:var(--sf-line);margin:6px 10px}
|
|
240
|
+
.settings-content{padding:24px;gap:20px}
|
|
241
|
+
.settings-content header{display:flex;flex-direction:column;gap:6px;padding-bottom:4px}
|
|
242
|
+
.settings-content h2{font:500 26px/1.15 'Funnel Sans';letter-spacing:0}
|
|
243
|
+
.settings-content .pane-description{margin:0;font-size:13px;color:var(--glass-secondary)}
|
|
244
|
+
.setting-group{border-radius:8px;background:rgba(237,235,231,.035);border:1px solid var(--sf-line)}
|
|
245
|
+
.setting-row{padding:12px;gap:12px}
|
|
246
|
+
.setting-row b{font-size:13px;font-weight:500;color:var(--sf-ink)}
|
|
247
|
+
.setting-row p{font-size:12.5px;color:var(--glass-secondary)}
|
|
248
|
+
.setting-row+.setting-row{border-top:0;box-shadow:inset 16px 1px 0 -15px transparent;position:relative}
|
|
249
|
+
.setting-row+.setting-row:before{content:'';position:absolute;left:16px;right:0;top:0;height:1px;background:var(--sf-line)}
|
|
250
|
+
.setting-row input[role=switch]{width:32px;height:19px;background:rgba(237,235,231,.18)}
|
|
251
|
+
.setting-row input[role=switch]:checked{background:var(--system-accent)}
|
|
252
|
+
.settings button.small{border-radius:6px;padding:4px 10px;font-size:12.5px;background:rgba(237,235,231,.10);border:0;box-shadow:inset 0 .5px 0 rgba(255,255,255,.14)}
|
|
253
|
+
.settings button.small:hover{background:rgba(237,235,231,.16)}
|
|
254
|
+
.settings .mono{font:12.5px 'Fragment Mono';font-variant-numeric:tabular-nums}
|
|
255
|
+
.settings a{display:inline-flex;align-items:center;gap:4px;font-size:13px;color:var(--sf-ink)}
|
|
256
|
+
.shortcut-recorder{padding:16px;display:flex;flex-direction:column;align-items:flex-start;gap:16px}
|
|
257
|
+
.shortcut-recorder b{font-size:14px;font-weight:500}
|
|
258
|
+
.shortcut-recorder .keycap{font:26px 'Fragment Mono';padding:16px 24px;border-radius:8px;background:rgba(237,235,231,.05);border:1px solid var(--sf-line-2);color:var(--sf-ink)}
|
|
259
|
+
.shortcut-recorder .keycap.recording{color:var(--glass-secondary);border-color:var(--system-accent)}
|
|
260
|
+
.shortcut-recorder p{font-size:12.5px;color:var(--glass-secondary)}
|
|
261
|
+
.shortcut-recorder .notice,.settings-error{display:flex;gap:6px;align-items:flex-start;color:var(--sf-warning);font-size:12.5px}
|
|
262
|
+
.setting-note{font-size:12.5px;color:var(--glass-secondary)}
|
|
263
|
+
.update-block{display:flex;flex-direction:column;gap:12px}
|
|
264
|
+
.update-block .update-notice{display:flex;align-items:center;gap:10px;padding:12px;border-radius:8px;background:var(--sf-overlay);border:1px solid var(--sf-line-2);font-size:13px;color:var(--sf-ink)}
|
|
265
|
+
.update-notice .warning{color:var(--sf-warning)}
|
|
266
|
+
.update-notice .success{color:var(--sf-success)}
|
|
267
|
+
.update-notice .secondary{color:var(--glass-secondary)}
|
|
268
|
+
.update-block small{font-size:11px;color:var(--glass-secondary)}
|
|
269
|
+
.settings button.primary.regular{border-radius:7px;padding:6px 14px;font-size:13px}
|
|
270
|
+
.own-shortcut kbd,.stage .keycaps kbd{font-family:'Funnel Sans';font-weight:500}
|
|
271
|
+
.own-shortcut kbd{font-size:14px;padding:6px 12px}
|
|
272
|
+
/* CredentialApprovalView: Vault and Login item inline in one group, Future requests as a menu, Deny as plain text. */
|
|
273
|
+
.permission-details.inline{gap:10px;padding:12px}
|
|
274
|
+
.permission label.inline-row{flex-direction:row;align-items:center;gap:12px}
|
|
275
|
+
.permission label.inline-row>span{width:72px;flex-shrink:0;font-size:12.5px;color:var(--glass-secondary)}
|
|
276
|
+
.permission label.inline-row.scope>span{width:auto;color:var(--sf-ink);font-size:13px}
|
|
277
|
+
.permission label.inline-row select,.permission label.inline-row input{flex:1}
|
|
278
|
+
.permission label.inline-row.scope select{flex:0 1 auto}
|
|
279
|
+
.permission input[name=vault-item]{height:30px;padding:0 10px;border-radius:6px;font-size:14px}
|
|
280
|
+
.permission button.plain-text{border:0;background:transparent;color:var(--glass-secondary);padding:6px 4px}
|
|
281
|
+
.permission button.primary{height:32px;padding:0 16px;font-weight:500}
|
|
282
|
+
.permission label.inline-row select{padding:4px 8px;height:28px}
|
package/app/ui/surfaces.mjs
CHANGED
|
@@ -66,7 +66,7 @@ export function installSurfaces({model,host,cli,save,savePrefs,render,button,reg
|
|
|
66
66
|
<button class="control project ${L.picker==='project'?'active':''} ${L.path?'':'empty'}" data-action="picker:project" title="${e(L.path?L.path:'Choose a project (Tab)')}" ${L.stage?'disabled':''}>${icon('folder',13)}<span class="project-name">${e(name)}</span>${icon('chevron-down',9)}</button>
|
|
67
67
|
<span class="grow"></span>
|
|
68
68
|
<span class="dropdowns">${pill('app',appMark(L.app,14),{title:`${L.app==='codex'?'Codex':'Claude'} conversations ${t3?'start':'open'} in ${place}`,label:'Agent app'})}${c?.models?.length?pill('model',`<span>${e(model_?.short||fallback?.short||'Default')}</span>`,{cls:model_?'':'secondary',title:`${model_?'Model':'Default model'} (Ctrl+Shift+M)`,label:'Model'}):''}${c&&efforts().length?pill('effort',`<span>${e(effort_?barEffort(effort_):effortDefault?barEffort(effortDefault):'Default')}</span>`,{cls:`${effort_?'':'secondary'} ${note?'dimmed':''}`,title:note?t3EffortLine[note]:`${effort_?'Effort':'Default effort'} (Ctrl+Shift+E)`,label:'Effort'}):''}</span>
|
|
69
|
-
<span class="tools">${L.targets?.computers===false?'':`<button class="control icon-only computer ${L.picker==='computer'?'active':''} ${L.on?'chosen':''}" data-action="picker:computer" title="${e((L.on?clean(L.computers.find(c=>c.workspace_id===L.on)?.label):'New computer. Choose one of this project’s computers')+' (Ctrl+Shift+C)')}" aria-label="${L.on?'Chosen computer':'New computer'}" ${L.stage||!L.path?'disabled':''}>${icon('desktop',15)}</button>`}<button class="control icon-only" data-action="capture" title="${L.images.length>=5?'Up to five captures':'Capture part of the screen (Ctrl+Shift+S)'}" aria-label="Capture part of the screen" ${L.stage||L.images.length>=5?'disabled':''}>${icon('capture',15)}</button></span>
|
|
69
|
+
<span class="tools">${L.targets?.computers===false?'':`<button class="control icon-only computer ${L.picker==='computer'?'active':''} ${L.on?'chosen':''}" data-action="picker:computer" title="${e((L.on?clean(L.computers.find(c=>c.workspace_id===L.on)?.label):'New computer. Choose one of this project’s computers')+' (Ctrl+Shift+C)')}" aria-label="${L.on?'Chosen computer':'New computer'}" ${L.stage||!L.path?'disabled':''}>${icon('desktop',15)}</button>`}<button class="control icon-only ${L.images.length>=5?'limit':''}" data-action="capture" title="${L.images.length>=5?'Up to five captures':'Capture part of the screen (Ctrl+Shift+S)'}" aria-label="Capture part of the screen" ${L.stage||L.images.length>=5?'disabled':''}>${icon('capture',15)}</button></span>
|
|
70
70
|
<button class="start ${canStart()?'':'off'}" data-action="start" title="${t3?'Start in T3 Code (Ctrl+Return)':'Start conversation (Ctrl+Return)'}" aria-label="${t3?'Start in T3 Code':'Start conversation'}" ${canStart()?'':'disabled'}>${t3Glyph}<span>${title}</span>${L.stage?'':`<span class="key-hint">Ctrl${icon('corner-down-left',11)}</span>`}</button>
|
|
71
71
|
</div>`;
|
|
72
72
|
}
|
|
@@ -92,7 +92,9 @@ export function installSurfaces({model,host,cli,save,savePrefs,render,button,reg
|
|
|
92
92
|
if(area==='project-search'||L.picker==='project'){const input=app.querySelector('[name=project-search]');if(input&&document.activeElement!==input){input.focus();input.setSelectionRange(input.value.length,input.value.length);}}
|
|
93
93
|
else if(area==='prompt'||!area){const t=app.querySelector('[name=prompt]');if(t&&!L.stage){t.focus();t.setSelectionRange(t.value.length,t.value.length);}}
|
|
94
94
|
}
|
|
95
|
-
|
|
95
|
+
// The project name gives way first when the row is full, truncated in the middle as on the Mac ("paymen…n-service").
|
|
96
|
+
function middle(){const row=app.querySelector('.launcher-controls'),name=app.querySelector('.project-name');if(!row||!name)return;const full=name.textContent;let keep=full.length;while(name.scrollWidth>name.clientWidth&&keep>4){keep--;const head=Math.ceil(keep/2),tail=Math.floor(keep/2);name.textContent=full.slice(0,head)+'…'+full.slice(full.length-tail);}}
|
|
97
|
+
function fit(){middle();requestAnimationFrame(()=>{const t=app.querySelector('textarea');if(t){t.style.height='auto';t.style.height=Math.min(96,t.scrollHeight)+'px';}const bar=app.querySelector('.launcher');if(bar)void host('resize',{height:Math.ceil(bar.getBoundingClientRect().height)+30});const s=app.querySelector('.result-row.selected');s?.scrollIntoView({block:'nearest'});});}
|
|
96
98
|
// A keystroke in the prompt updates only what it changes (Start, and the matching conversations), never the field.
|
|
97
99
|
function promptChanged(){
|
|
98
100
|
const start=app.querySelector('[data-action=start]');if(start){start.disabled=!canStart();start.classList.toggle('off',!canStart());}
|
package/app/ui/tray.mjs
CHANGED
|
@@ -53,7 +53,7 @@ export function installTray({model,host,cli,render,button,registerActions,perfor
|
|
|
53
53
|
app.addEventListener('contextmenu',ev=>{if(model.view!=='tray')return;const el=ev.target.closest('[data-row]');ev.preventDefault();if(!el)return;const r=(model.rows||[]).find(r=>r.id===el.dataset.row);if(!r)return;
|
|
54
54
|
void host('popup',{x:Math.round(ev.clientX),y:Math.round(ev.clientY),width:1,height:1,items:[{label:'Open',action:`open:${r.reference}`},{label:'New thread on this computer',action:`thread:${r.id}`},{label:'Archive',action:`archive:${r.id}`},{separator:true},{label:'Copy workspace key',action:`copy-key:${r.workspace_key}`}]});});
|
|
55
55
|
// Updates.swift: the CLI's update check, at start and then every six hours; a newer release puts the dot on ⋯.
|
|
56
|
-
async function checkUpdates(){try{(model.settings??={}).update=await cli(['update','--check','--json']);if(model.view==='tray')render();}catch{}}
|
|
56
|
+
async function checkUpdates(){try{(model.settings??={}).update=await cli(['update','--check','--json']);model.settings.checkedAt=Date.now();if(model.view==='tray')render();}catch{}}
|
|
57
57
|
window.sfCheckUpdates=checkUpdates;void checkUpdates();setInterval(checkUpdates,6*3600*1000);
|
|
58
58
|
window.addEventListener('keydown',ev=>{if(model.view!=='tray')return;
|
|
59
59
|
if(ev.key==='Escape'){if(T.overflow||T.actions){T.overflow=false;T.actions=null;render();}else void host('hide');}
|