@shardflux/desktop-linux-arm64 0.0.0-stage → 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/CHANGELOG.md +7 -0
- package/LICENSE +202 -0
- package/README.md +2 -2
- package/app/manifest.json +1 -0
- package/app/shardflux-desktop +0 -0
- package/app/ui/agents.mjs +21 -0
- package/app/ui/app.mjs +78 -0
- package/app/ui/assets/FragmentMono-OFL.txt +93 -0
- package/app/ui/assets/FragmentMono.ttf +0 -0
- package/app/ui/assets/FunnelDisplay-OFL.txt +93 -0
- package/app/ui/assets/FunnelDisplay.woff2 +0 -0
- package/app/ui/assets/FunnelSans-OFL.txt +93 -0
- package/app/ui/assets/FunnelSans.ttf +0 -0
- package/app/ui/assets/Lucide-LICENSE +15 -0
- package/app/ui/assets/README.md +5 -0
- package/app/ui/assets/claude-mark.png +0 -0
- package/app/ui/assets/onboarding-silk.jpg +0 -0
- package/app/ui/assets/onboarding-wash-warm.webp +0 -0
- package/app/ui/assets/openai-mark.png +0 -0
- package/app/ui/assets/shardflux-icon.png +0 -0
- package/app/ui/assets/shardflux-icon.svg +1 -0
- package/app/ui/assets/shardflux-mark.svg +1 -0
- package/app/ui/assets/shardflux-tray.png +0 -0
- package/app/ui/assets/shardflux-tray.svg +1 -0
- package/app/ui/core.mjs +129 -0
- package/app/ui/icons.mjs +63 -0
- package/app/ui/index.html +1 -0
- package/app/ui/integrations.mjs +35 -0
- package/app/ui/settings.mjs +41 -0
- package/app/ui/style.css +282 -0
- package/app/ui/surfaces.mjs +221 -0
- package/app/ui/tokens.css +149 -0
- package/app/ui/tray.mjs +61 -0
- package/package.json +22 -4
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
Copyright 2024 The Funnel Project Authors (https://github.com/Dicotype/Funnel)
|
|
2
|
+
|
|
3
|
+
This Font Software is licensed under the SIL Open Font License, Version 1.1.
|
|
4
|
+
This license is copied below, and is also available with a FAQ at:
|
|
5
|
+
https://openfontlicense.org
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
-----------------------------------------------------------
|
|
9
|
+
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
|
|
10
|
+
-----------------------------------------------------------
|
|
11
|
+
|
|
12
|
+
PREAMBLE
|
|
13
|
+
The goals of the Open Font License (OFL) are to stimulate worldwide
|
|
14
|
+
development of collaborative font projects, to support the font creation
|
|
15
|
+
efforts of academic and linguistic communities, and to provide a free and
|
|
16
|
+
open framework in which fonts may be shared and improved in partnership
|
|
17
|
+
with others.
|
|
18
|
+
|
|
19
|
+
The OFL allows the licensed fonts to be used, studied, modified and
|
|
20
|
+
redistributed freely as long as they are not sold by themselves. The
|
|
21
|
+
fonts, including any derivative works, can be bundled, embedded,
|
|
22
|
+
redistributed and/or sold with any software provided that any reserved
|
|
23
|
+
names are not used by derivative works. The fonts and derivatives,
|
|
24
|
+
however, cannot be released under any other type of license. The
|
|
25
|
+
requirement for fonts to remain under this license does not apply
|
|
26
|
+
to any document created using the fonts or their derivatives.
|
|
27
|
+
|
|
28
|
+
DEFINITIONS
|
|
29
|
+
"Font Software" refers to the set of files released by the Copyright
|
|
30
|
+
Holder(s) under this license and clearly marked as such. This may
|
|
31
|
+
include source files, build scripts and documentation.
|
|
32
|
+
|
|
33
|
+
"Reserved Font Name" refers to any names specified as such after the
|
|
34
|
+
copyright statement(s).
|
|
35
|
+
|
|
36
|
+
"Original Version" refers to the collection of Font Software components as
|
|
37
|
+
distributed by the Copyright Holder(s).
|
|
38
|
+
|
|
39
|
+
"Modified Version" refers to any derivative made by adding to, deleting,
|
|
40
|
+
or substituting -- in part or in whole -- any of the components of the
|
|
41
|
+
Original Version, by changing formats or by porting the Font Software to a
|
|
42
|
+
new environment.
|
|
43
|
+
|
|
44
|
+
"Author" refers to any designer, engineer, programmer, technical
|
|
45
|
+
writer or other person who contributed to the Font Software.
|
|
46
|
+
|
|
47
|
+
PERMISSION & CONDITIONS
|
|
48
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
|
49
|
+
a copy of the Font Software, to use, study, copy, merge, embed, modify,
|
|
50
|
+
redistribute, and sell modified and unmodified copies of the Font
|
|
51
|
+
Software, subject to the following conditions:
|
|
52
|
+
|
|
53
|
+
1) Neither the Font Software nor any of its individual components,
|
|
54
|
+
in Original or Modified Versions, may be sold by itself.
|
|
55
|
+
|
|
56
|
+
2) Original or Modified Versions of the Font Software may be bundled,
|
|
57
|
+
redistributed and/or sold with any software, provided that each copy
|
|
58
|
+
contains the above copyright notice and this license. These can be
|
|
59
|
+
included either as stand-alone text files, human-readable headers or
|
|
60
|
+
in the appropriate machine-readable metadata fields within text or
|
|
61
|
+
binary files as long as those fields can be easily viewed by the user.
|
|
62
|
+
|
|
63
|
+
3) No Modified Version of the Font Software may use the Reserved Font
|
|
64
|
+
Name(s) unless explicit written permission is granted by the corresponding
|
|
65
|
+
Copyright Holder. This restriction only applies to the primary font name as
|
|
66
|
+
presented to the users.
|
|
67
|
+
|
|
68
|
+
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
|
|
69
|
+
Software shall not be used to promote, endorse or advertise any
|
|
70
|
+
Modified Version, except to acknowledge the contribution(s) of the
|
|
71
|
+
Copyright Holder(s) and the Author(s) or with their explicit written
|
|
72
|
+
permission.
|
|
73
|
+
|
|
74
|
+
5) The Font Software, modified or unmodified, in part or in whole,
|
|
75
|
+
must be distributed entirely under this license, and must not be
|
|
76
|
+
distributed under any other license. The requirement for fonts to
|
|
77
|
+
remain under this license does not apply to any document created
|
|
78
|
+
using the Font Software.
|
|
79
|
+
|
|
80
|
+
TERMINATION
|
|
81
|
+
This license becomes null and void if any of the above conditions are
|
|
82
|
+
not met.
|
|
83
|
+
|
|
84
|
+
DISCLAIMER
|
|
85
|
+
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
|
86
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
|
|
87
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
|
|
88
|
+
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
|
|
89
|
+
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
|
90
|
+
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
|
|
91
|
+
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
|
92
|
+
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
|
|
93
|
+
OTHER DEALINGS IN THE FONT SOFTWARE.
|
|
Binary file
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
ISC License
|
|
2
|
+
|
|
3
|
+
Copyright (c) for portions of Lucide are held by Cole Bemis 2013-2022 as part of Feather (MIT). All other copyright (c) for Lucide are held by Lucide Contributors 2022.
|
|
4
|
+
|
|
5
|
+
Permission to use, copy, modify, and/or distribute this software for any
|
|
6
|
+
purpose with or without fee is hereby granted, provided that the above
|
|
7
|
+
copyright notice and this permission notice appear in all copies.
|
|
8
|
+
|
|
9
|
+
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
|
|
10
|
+
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
|
11
|
+
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
|
|
12
|
+
ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
|
13
|
+
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
|
|
14
|
+
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
|
|
15
|
+
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
The mark, onboarding images and fonts are the same approved assets as the macOS app.
|
|
2
|
+
Publisher marks are unchanged Claude and OpenAI assets. Font licenses accompany the fonts.
|
|
3
|
+
`shardflux-icon.png` and `shardflux-tray.png` are rasterizations of the adjacent approved
|
|
4
|
+
SVGs (librsvg `rsvg-convert -w 128 -h 128` and `-w 32 -h 32` respectively) for native
|
|
5
|
+
notification and StatusNotifierItem hosts that do not load SVG images.
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32" width="32" height="32" fill="#edebe7"><rect x="0" y="0" width="32" height="32" rx="7" fill="#111113"/><g transform="rotate(18 16 16)"><polygon points="16,1 11.13,12.4 20.87,12.4 16,1"/><polygon points="10.71,13.4 9,17.4 9.77,18.9 22.23,18.9 23,17.4 21.29,13.4"/><polygon points="10.29,19.9 12.86,24.9 19.14,24.9 21.71,19.9" opacity="0.72"/><polygon points="13.38,25.9 16,31 16,31 18.63,25.9" opacity="0.45"/></g></svg>
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32" width="32" height="32" fill="currentColor"><g transform="rotate(18 16 16)"><polygon points="16,1 11.13,12.4 20.87,12.4 16,1"/><polygon points="10.71,13.4 9,17.4 9.77,18.9 22.23,18.9 23,17.4 21.29,13.4"/><polygon points="10.29,19.9 12.86,24.9 19.14,24.9 21.71,19.9" opacity="0.72"/><polygon points="13.38,25.9 16,31 16,31 18.63,25.9" opacity="0.45"/></g></svg>
|
|
Binary file
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32" width="32" height="32" fill="#edebe7"><g transform="rotate(18 16 16)"><polygon points="16,1 11.13,12.4 20.87,12.4 16,1"/><polygon points="10.71,13.4 9,17.4 9.77,18.9 22.23,18.9 23,17.4 21.29,13.4"/><polygon points="10.29,19.9 12.86,24.9 19.14,24.9 21.71,19.9" opacity="0.72"/><polygon points="13.38,25.9 16,31 16,31 18.63,25.9" opacity="0.45"/></g></svg>
|
package/app/ui/core.mjs
ADDED
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
// Port of MenuBarCore/Onboarding.swift. These functions have no UI or platform dependencies.
|
|
2
|
+
export const steps = ['Sign in', 'Agents', 'Try it once', 'Your own project'];
|
|
3
|
+
export const samplePrompt = 'Start Daylight and test it in the browser, then tell me what works.';
|
|
4
|
+
export function progress(api) { return {version:4, api_url:api, step:0, dismissed:false, finished:false, apps:[], demo_copied:false, demo_finished:false, demo_changed:false, feed:[], completed:[]}; }
|
|
5
|
+
export function readProgress(value, api) {
|
|
6
|
+
if (value?.version !== 4 || value.api_url !== api) return progress(api);
|
|
7
|
+
return {...progress(api), ...value, apps:(value.apps ?? []).filter(a=>['claude','codex'].includes(a)),feed:(value.feed ?? []).filter(e=>e&&typeof e==='object'&&typeof e.text==='string')};
|
|
8
|
+
}
|
|
9
|
+
export const accountConnected = inspection => inspection?.connections?.some(c=>c.id==='account' && c.state==='verified') ?? false;
|
|
10
|
+
export const anyAgentConnected = inspection => accountConnected(inspection) && inspection.connections.some(c=>['claude','codex'].includes(c.id) && c.state==='verified');
|
|
11
|
+
export function canContinue(p, inspection) { return [accountConnected(inspection), anyAgentConnected(inspection), p.demo_finished, !!p.own_key][p.step]; }
|
|
12
|
+
export function demoPhase(p, launcher) { return p.demo_key ? p.demo_finished ? 'result' : 'watch' : !p.demo_copied ? 'copy' : launcher ? 'start' : 'launcher'; }
|
|
13
|
+
export function recordStart(p, key, path, app) {
|
|
14
|
+
const same = a=>a?.replace(/\/+$/, '') === path.replace(/\/+$/, '');
|
|
15
|
+
if (p.step===2 && !p.demo_key) {
|
|
16
|
+
if (!same(p.sample_path)) return 'otherProject';
|
|
17
|
+
Object.assign(p,{demo_key:key,demo_app:app,demo_finished:false,shot:null,demo_changed:false,feed:[],feed_baseline:undefined,demo_workspace:null});return 'demo';
|
|
18
|
+
}
|
|
19
|
+
if (p.step===3 && !p.own_key && !same(p.sample_path)) {Object.assign(p,{own_key:key,own_path:path,own_app:app});p.completed=[...new Set([...p.completed,3])];return 'own';}
|
|
20
|
+
return 'ignored';
|
|
21
|
+
}
|
|
22
|
+
export function cardState(c) {
|
|
23
|
+
if(!c) return {kind:'checking',fact:'Checking…'};
|
|
24
|
+
if(c.state==='verified')return {kind:'connected',fact:c.fact};
|
|
25
|
+
if(c.state==='pending')return {kind:'connecting',fact:c.fact};
|
|
26
|
+
if(c.state==='unknown'||c.action?.retry)return {kind:'retry',fact:c.fact,button:'Retry'};
|
|
27
|
+
if(c.state==='info')return {kind:'info',fact:c.fact};
|
|
28
|
+
const title=c.action?.title;
|
|
29
|
+
return {kind:'action',fact:c.fact,button:['Sign in to Claude','Connect Codex','Sign in to Codex'].includes(title)||!title?'Connect':title};
|
|
30
|
+
}
|
|
31
|
+
export function remainingConnecting(connecting, inspection, connected) {
|
|
32
|
+
return connecting.filter(app=>{const c=inspection.connections.find(c=>c.id===app);return app==='t3'||!c||!(c.state==='verified'||c.action?.retry||(connected&&app==='codex'&&c.state!=='pending'));});
|
|
33
|
+
}
|
|
34
|
+
export class WorkWatch {worked=false;observe(state){if(state==='working')this.worked=true;}finished(state,message){return state!=='working'&&(this.worked||!!message);} }
|
|
35
|
+
export class DemoTurn {
|
|
36
|
+
conversation=null;row=null;conversationWatch=new WorkWatch();rowWatch=new WorkWatch();
|
|
37
|
+
observeConversation(c){this.conversation=c;this.conversationWatch.observe(c.state);}
|
|
38
|
+
observeRow(r){this.row=r;this.rowWatch.observe(r.state);}
|
|
39
|
+
get finished(){if(this.conversation)return ['needs_you','unresolved','done','archived'].includes(this.conversation.state)&&this.conversationWatch.finished('waiting',this.conversation.summary);return this.row?this.rowWatch.finished(this.row.state,this.row.last_message):false;}
|
|
40
|
+
}
|
|
41
|
+
export const commands = {
|
|
42
|
+
inspect:(connect=false,retry=false)=>['setup','--inspect','--json','--apps','claude,codex',...(connect?['--connect']:[]),...(retry?['--retry-codex']:[])],
|
|
43
|
+
browser:name=>['auth','login','--browser','--accept-terms','--client-name',name,'--json'],
|
|
44
|
+
signIn:['signup','--accept-terms','--no-setup','--json'],
|
|
45
|
+
mfa:code=>{const v=code.trim(),digits=v.replace(/ /g,'');return ['auth','mfa',...(/^[0-9]{6}$/.test(digits)?['--code',digits]:['--recovery-code',v]),'--json'];},
|
|
46
|
+
setup:['setup','--json','--no-codex'],sample:['setup','--prepare-sample','--json'],
|
|
47
|
+
activity:key=>['setup','--inspect','--activity','--workspace',key,'--json'],
|
|
48
|
+
stream:key=>['workspaces','computer','stream',key,'--ttl','1h','--json'],
|
|
49
|
+
screenshot:(key,out)=>['workspaces','computer','screenshot',key,'--out',out,'--json'],
|
|
50
|
+
create:({app,path,promptFile,computer,on,model,effort,t3,images=[]})=>['agents','new','--app',app,'--path',path,...(t3?['--in','t3']:[]),...(on?['--on',on]:[]),...(promptFile?['--prompt-file',promptFile]:[]),...(computer?['--computer']:[]),...images.flatMap(path=>['--image',path]),...(model?['--model',model]:[]),...(effort?['--effort',effort]:[]),'--json'],
|
|
51
|
+
open:(key,target='auto')=>['agents','open',key,'--in',target,'--json']
|
|
52
|
+
};
|
|
53
|
+
export function reference(doc,joined=false) {
|
|
54
|
+
if(joined||doc.conversation?.route==='codex-cloud'){if(/^conv_[a-z0-9]{26}$/.test(doc.conversation?.id))return doc.conversation.id;}
|
|
55
|
+
if(doc.workspace?.workspace_key)return doc.workspace.workspace_key;
|
|
56
|
+
throw new Error('shard did not return a conversation destination. Update Shardflux and try Open again.');
|
|
57
|
+
}
|
|
58
|
+
export function browserEvent(line) {
|
|
59
|
+
try {const e=JSON.parse(line);if(e.event==='open' && e.user_code)return {...e,verification_uri_complete:safeHTTPS(e.verification_uri_complete)};if(['signed_in','error'].includes(e.event))return e;}catch{}return null;
|
|
60
|
+
}
|
|
61
|
+
export function safeHTTPS(text) {try{const u=new URL(text);return u.protocol==='https:'&&!u.username&&!u.password?u.href:null;}catch{return null;} }
|
|
62
|
+
export function streamURL(doc){const url=safeHTTPS(doc.url);if(doc.interactive!==false||!url)throw new Error('The live view didn’t open. Try again.');return url;}
|
|
63
|
+
export function fold(text){return text.normalize('NFD').replace(/[\u0300-\u036f]/g,'').toLowerCase();}
|
|
64
|
+
export function matches(text,query){text=fold(text);let i=0;for(const c of fold(query).replace(/\s/g,'')){i=text.indexOf(c,i);if(i<0)return false;i++;}return true;}
|
|
65
|
+
export function validChoice(choice,catalog) {const model=catalog?.models?.some(m=>m.id===choice.model)?choice.model:null;const efforts=catalog?.models?.find(m=>m.id===model)?.efforts??catalog?.efforts??[];return {model,effort:efforts.some(e=>(e.id??e)===choice.effort)?choice.effort:null};}
|
|
66
|
+
export const escape = value=>String(value??'').replace(/[&<>"']/g,c=>({'&':'&','<':'<','>':'>','"':'"',"'":'''}[c]));
|
|
67
|
+
export function composePrompt(task,branch='',link='',files=[]){
|
|
68
|
+
if(!task.trim())throw new Error('Describe what the agent should do.');
|
|
69
|
+
const text=[task,...(branch?[`Branch context: ${branch}`]:[]),...(link?[`Link context: ${link}`]:[]),...files.map(f=>`File context: ${f.path.split('/').at(-1)}\n${f.text}`)].join('\n\n');
|
|
70
|
+
if(new TextEncoder().encode(text).length>262144||/[\0\x1b]/.test(text))throw new Error('Keep the task and context under 256 KiB, without terminal control characters.');
|
|
71
|
+
return text;
|
|
72
|
+
}
|
|
73
|
+
export function modelEfforts(catalog,model){const chosen=catalog?.models?.find(m=>m.id===(model||catalog.default?.model));return (catalog?.efforts||[]).filter(e=>!chosen?.efforts||chosen.efforts.includes(e.id));}
|
|
74
|
+
// MenuBarCore/DesktopStream.swift ActivityFeed: what the agent did in its own computer, as a person would say it.
|
|
75
|
+
export const listNames=items=>items.length<2?(items[0]??''):`${items.slice(0,-1).join(', ')} and ${items.at(-1)}`;
|
|
76
|
+
const blankPages=new Set(['Untitled','New Tab','about:blank']);
|
|
77
|
+
export function pageTitle(window){for(const suffix of [' - Google Chrome for Testing',' - Google Chrome',' - Chromium',' — Mozilla Firefox',' - Mozilla Firefox'])if(window.endsWith(suffix))return window.slice(0,-suffix.length);return window;}
|
|
78
|
+
export function observeActivity(feed,activity){
|
|
79
|
+
const events=feed.events??=[],same=(a,b)=>a.kind===b.kind&&a.text===b.text&&a.detail===b.detail,add=e=>{if(!events.some(x=>same(x,e)))events.push(e);};
|
|
80
|
+
for(const command of activity.commands||[])if(command.startsWith('sudo ')&&command.includes(' install')){const words=command.split(' '),packages=words.slice(words.indexOf('install')+1).filter(w=>w&&!w.startsWith('-')&&!w.includes('='));if(!packages.length)continue;const names=listNames(packages);if(!events.some(e=>e.kind==='install'&&e.detail===names))add({kind:'install',text:`Installing ${names} with sudo`,detail:names});}
|
|
81
|
+
for(const install of activity.installs||[]){const names=listNames(install.packages||[]),done={kind:'install',text:`Installed ${names}${install.sudo?' with sudo':''}`,detail:names},i=events.findIndex(e=>e.kind==='install'&&e.detail===names);if(i>=0)events[i]=done;else add(done);}
|
|
82
|
+
for(const command of (activity.commands||[]).slice(0,1))if(command.includes(' ')&&!command.startsWith('apt')&&!command.startsWith('sudo ')&&!command.startsWith('dpkg'))add({kind:'command',text:`Started ${command}`,detail:command});
|
|
83
|
+
for(const port of activity.ports||[])add({kind:'port',text:`Opened port ${port}`,detail:String(port)});
|
|
84
|
+
const page=pageTitle(activity.browser?.title||'');
|
|
85
|
+
if(page&&!blankPages.has(page)&&events.findLast(e=>e.kind==='browser')?.detail!==page){const [app,view='']=page.split(' · '),browsing=events.filter(e=>e.kind==='browser');
|
|
86
|
+
if(!browsing.length)add({kind:'browser',text:`Opened ${app} in its browser`,detail:page});
|
|
87
|
+
else if(browsing[0].detail===page||!view)events.push({kind:'browser',text:`Went back to ${app}’s first screen`,detail:page});
|
|
88
|
+
else events.push({kind:'browser',text:`Clicked ${view}`,detail:page});}
|
|
89
|
+
if(feed.baseline===undefined)feed.baseline=activity.changes?.commits??0;
|
|
90
|
+
if((activity.changes?.files??0)>0||(activity.changes?.commits??0)>feed.baseline)feed.changed=true;
|
|
91
|
+
return feed;
|
|
92
|
+
}
|
|
93
|
+
export function recap(feed){
|
|
94
|
+
const events=feed.events||[],lines=[],installs=events.filter(e=>e.kind==='install'&&e.text.startsWith('Installed'));
|
|
95
|
+
if(installs.length)lines.push(installs.map(e=>e.text).join('; '));
|
|
96
|
+
const command=events.find(e=>e.kind==='command'&&!e.detail.startsWith('sudo '))?.detail,port=events.find(e=>e.kind==='port')?.detail;
|
|
97
|
+
if(command&&port)lines.push(`Started ${command} on :${port}`);else if(command)lines.push(`Started ${command}`);else if(port)lines.push(`Opened port ${port}`);
|
|
98
|
+
const pages=new Set(events.filter(e=>e.kind==='browser').map(e=>e.detail)).size;
|
|
99
|
+
if(pages)lines.push(pages===1?'Tested it in its browser':`Tested ${pages} screens in its browser`);
|
|
100
|
+
return lines;
|
|
101
|
+
}
|
|
102
|
+
// OpenTarget.place, as Linux opens conversations: its terminal, its browser (claude.ai) or T3 Code.
|
|
103
|
+
export function openPlace(targets,app){const target=targets?.open_in?.[app]||(app==='codex'?'terminal':'terminal');return {terminal:'your terminal',web:'your browser',t3:'T3 Code'}[target]||'your terminal';}
|
|
104
|
+
// A GTK accelerator ("<Alt><Shift>space") as the app shows it ("Alt+Shift+Space").
|
|
105
|
+
export function shortcutLabel(accelerator='<Alt><Shift>space'){const mods=[...accelerator.matchAll(/<(\w+)>/g)].map(m=>({Control:'Ctrl',Primary:'Ctrl',Alt:'Alt',Shift:'Shift',Super:'Super'}[m[1]]||m[1]));const key=accelerator.replace(/<\w+>/g,'');return [...mods,key.length===1?key.toUpperCase():key[0].toUpperCase()+key.slice(1)].join('+');}
|
|
106
|
+
// MenuBarCore/AgentTargets.swift T3Failure: a refused T3 Code hand-off is one line and the one action that helps. When
|
|
107
|
+
// the thread already started (details.t3_thread_id), that action opens it: starting again would make a second one.
|
|
108
|
+
export function t3Failure(detail){
|
|
109
|
+
if(typeof detail?.code!=='string'||!detail.code.startsWith('t3_')||!detail.message)return null;
|
|
110
|
+
const d=detail.details||{},text=k=>typeof d[k]==='string'&&d[k]?d[k]:null,conversation=text('conversation_id');
|
|
111
|
+
const fix=typeof detail.fix==='string'?detail.fix:detail.fix?.message??detail.fix?.title??null;
|
|
112
|
+
return {code:detail.code,message:detail.message,fix:fix||null,threadID:text('t3_thread_id'),conversation:/^conv_[a-z0-9]{26}$/.test(conversation||'')?conversation:null,workspaceKey:text('workspace_key')};
|
|
113
|
+
}
|
|
114
|
+
export const failureReference=f=>f?.conversation??f?.workspaceKey??null;
|
|
115
|
+
export function t3Remedy(f,mode){
|
|
116
|
+
if(!f)return null;if(f.threadID)return 'openThread';
|
|
117
|
+
switch(f.code){case 't3_server_unreachable':return mode==='ssh'?null:'openT3';case 't3_version_unsupported':return f.fix?.includes('shard update')?'updateShardflux':'openT3';case 't3_not_connected':case 't3_not_installed':return 'settings';default:return null;}
|
|
118
|
+
}
|
|
119
|
+
export function t3RemedyTitle(f,mode){const r=t3Remedy(f,mode);return r?{openThread:'Open',openT3:f.code==='t3_version_unsupported'?'Update T3 Code':'Open T3 Code',updateShardflux:'Update Shardflux',settings:f.code==='t3_not_connected'?'Connect T3 Code':'Choose where it opens'}[r]:null;}
|
|
120
|
+
// T3EffortReason: why a picked effort reached T3 Code as none. The effort pill's tooltip, and the line after the hand-off.
|
|
121
|
+
export const t3EffortLine={not_offered:'T3 Code sets the effort for this model',models_not_known:'T3 Code hasn’t listed this model’s efforts yet'};
|
|
122
|
+
export function effortNotApplied(doc,picked){if(!picked||doc?.effort!=null)return null;return t3EffortLine[doc?.effort_reason]?doc.effort_reason:'not_offered';}
|
|
123
|
+
export const effortKey=(app,model)=>`${app}|${model||'default'}`;
|
|
124
|
+
// OpenTarget.effective: T3 Code only while it is installed and connected; otherwise the app's own place.
|
|
125
|
+
export function openTarget(targets,app){const chosen=targets?.open_in?.[app];if(chosen==='t3')return targets?.t3?.installed!==false&&targets?.t3?.connected?'t3':'terminal';return chosen||'terminal';}
|
|
126
|
+
// LauncherView.barEffort: "Extra high" is "XHigh" in the bar (the menu keeps the whole name).
|
|
127
|
+
export const barEffort=e=>e?.id==='xhigh'?'XHigh':e?.label??'Default';
|
|
128
|
+
// LauncherProjectList.Entry: the folder's name, and its parent folder as the detail.
|
|
129
|
+
export function projectEntry(path){const parts=path.replace(/\/+$/,'').split('/').filter(Boolean);return {name:parts.at(-1)||path,detail:parts.length>1?parts.at(-2):'',path};}
|
package/app/ui/icons.mjs
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
// Lucide 0.468.0 (ISC; sources and licence in vendor/lucide), drawn at 1.5 px like SF Symbols' regular weight.
|
|
2
|
+
// Each name notes the SF Symbol it stands in for in the Mac app.
|
|
3
|
+
const paths={
|
|
4
|
+
search:'<circle cx="11" cy="11" r="8"/><path d="m21 21-4.3-4.3"/>', // magnifyingglass
|
|
5
|
+
folder:'<path d="M20 20a2 2 0 0 0 2-2V8a2 2 0 0 0-2-2h-7.9a2 2 0 0 1-1.69-.9L9.6 3.9A2 2 0 0 0 7.93 3H4a2 2 0 0 0-2 2v13a2 2 0 0 0 2 2Z"/>',
|
|
6
|
+
'folder-plus':'<path d="M12 10v6"/><path d="M9 13h6"/><path d="M20 20a2 2 0 0 0 2-2V8a2 2 0 0 0-2-2h-7.9a2 2 0 0 1-1.69-.9L9.6 3.9A2 2 0 0 0 7.93 3H4a2 2 0 0 0-2 2v13a2 2 0 0 0 2 2Z"/>', // folder.badge.plus
|
|
7
|
+
'chevron-down':'<path d="m6 9 6 6 6-6"/>',
|
|
8
|
+
'chevron-right':'<path d="m9 18 6-6-6-6"/>',
|
|
9
|
+
'chevron-left':'<path d="m15 18-6-6 6-6"/>',
|
|
10
|
+
plus:'<path d="M5 12h14"/><path d="M12 5v14"/>',
|
|
11
|
+
monitor:'<rect width="20" height="14" x="2" y="3" rx="2"/><line x1="8" x2="16" y1="21" y2="21"/><line x1="12" x2="12" y1="17" y2="21"/>',
|
|
12
|
+
// desktopcomputer: Lucide monitor with the solid chin SF Symbols draws under the screen.
|
|
13
|
+
desktop:'<rect width="20" height="14" x="2" y="3" rx="2"/><path d="M2.75 13.25h18.5V15a1.25 1.25 0 0 1-1.25 1.25H4A1.25 1.25 0 0 1 2.75 15z" fill="currentColor" stroke="none"/><line x1="8" x2="16" y1="21" y2="21"/><line x1="12" x2="12" y1="17" y2="21"/>',
|
|
14
|
+
'corner-down-left':'<polyline points="9 10 4 15 9 20"/><path d="M20 4v7a4 4 0 0 1-4 4H4"/>', // return (the ↵ of ⌘↵)
|
|
15
|
+
// camera.viewfinder: Lucide scan's corners around Lucide camera at half size (its stroke doubled to stay 1.5 px).
|
|
16
|
+
capture:'<path d="M3 7V5a2 2 0 0 1 2-2h2"/><path d="M17 3h2a2 2 0 0 1 2 2v2"/><path d="M21 17v2a2 2 0 0 1-2 2h-2"/><path d="M7 21H5a2 2 0 0 1-2-2v-2"/><g transform="translate(6 6) scale(.5)" stroke-width="3"><path d="M14.5 4h-5L7 7H4a2 2 0 0 0-2 2v9a2 2 0 0 0 2 2h16a2 2 0 0 0 2-2V9a2 2 0 0 0-2-2h-3l-2.5-3z"/><circle cx="12" cy="13" r="3"/></g>',
|
|
17
|
+
check:'<path d="M20 6 9 17l-5-5"/>', // checkmark
|
|
18
|
+
'circle-check':'<circle cx="12" cy="12" r="10"/><path d="m9 12 2 2 4-4"/>',
|
|
19
|
+
// checkmark.circle.fill: the circle filled with the current colour, the check cut out in the card's ink.
|
|
20
|
+
'circle-check-fill':'<circle cx="12" cy="12" r="10" fill="currentColor"/><path d="m8.25 12.25 2.5 2.5 5-5.25" stroke="#0b0b0c" stroke-width="2.25"/>',
|
|
21
|
+
ellipsis:'<circle cx="12" cy="12" r="1"/><circle cx="19" cy="12" r="1"/><circle cx="5" cy="12" r="1"/>',
|
|
22
|
+
settings:'<path d="M12.22 2h-.44a2 2 0 0 0-2 2v.18a2 2 0 0 1-1 1.73l-.43.25a2 2 0 0 1-2 0l-.15-.08a2 2 0 0 0-2.73.73l-.22.38a2 2 0 0 0 .73 2.73l.15.1a2 2 0 0 1 1 1.72v.51a2 2 0 0 1-1 1.74l-.15.09a2 2 0 0 0-.73 2.73l.22.38a2 2 0 0 0 2.73.73l.15-.08a2 2 0 0 1 2 0l.43.25a2 2 0 0 1 1 1.73V20a2 2 0 0 0 2 2h.44a2 2 0 0 0 2-2v-.18a2 2 0 0 1 1-1.73l.43-.25a2 2 0 0 1 2 0l.15.08a2 2 0 0 0 2.73-.73l.22-.39a2 2 0 0 0-.73-2.73l-.15-.08a2 2 0 0 1-1-1.74v-.5a2 2 0 0 1 1-1.74l.15-.09a2 2 0 0 0 .73-2.73l-.22-.38a2 2 0 0 0-2.73-.73l-.15.08a2 2 0 0 1-2 0l-.43-.25a2 2 0 0 1-1-1.73V4a2 2 0 0 0-2-2z"/><circle cx="12" cy="12" r="3"/>', // gearshape
|
|
23
|
+
'arrow-right':'<path d="M5 12h14"/><path d="m12 5 7 7-7 7"/>',
|
|
24
|
+
'arrow-up-right':'<path d="M7 7h10v10"/><path d="M7 17 17 7"/>',
|
|
25
|
+
'square-arrow-up-right':'<rect width="18" height="18" x="3" y="3" rx="2"/><path d="M8 8h8v8"/><path d="m8 16 8-8"/>', // arrow.up.forward.square
|
|
26
|
+
x:'<path d="M18 6 6 18"/><path d="m6 6 12 12"/>', // xmark
|
|
27
|
+
'circle-x':'<circle cx="12" cy="12" r="10"/><path d="m15 9-6 6"/><path d="m9 9 6 6"/>', // xmark.circle
|
|
28
|
+
'circle-alert':'<circle cx="12" cy="12" r="10"/><line x1="12" x2="12" y1="8" y2="12"/><line x1="12" x2="12.01" y1="16" y2="16"/>', // exclamationmark.circle
|
|
29
|
+
info:'<circle cx="12" cy="12" r="10"/><path d="M12 16v-4"/><path d="M12 8h.01"/>', // info.circle
|
|
30
|
+
copy:'<rect width="14" height="14" x="8" y="8" rx="2" ry="2"/><path d="M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2"/>', // doc.on.doc
|
|
31
|
+
archive:'<rect width="20" height="5" x="2" y="3" rx="1"/><path d="M4 8v11a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8"/><path d="M10 12h4"/>', // archivebox
|
|
32
|
+
file:'<path d="M15 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V7Z"/><path d="M14 2v4a2 2 0 0 0 2 2h4"/>', // doc
|
|
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
|
+
'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
|
+
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"/>',
|
|
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"/>',
|
|
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
|
|
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
|
|
42
|
+
hourglass:'<path d="M5 22h14"/><path d="M5 2h14"/><path d="M17 22v-4.172a2 2 0 0 0-.586-1.414L12 12l-4.414 4.414A2 2 0 0 0 7 17.828V22"/><path d="M7 2v4.172a2 2 0 0 0 .586 1.414L12 12l4.414-4.414A2 2 0 0 0 17 6.172V2"/>',
|
|
43
|
+
// lock.shield: Lucide shield with Lucide lock at half size inside it.
|
|
44
|
+
'shield-lock':'<path d="M20 13c0 5-3.5 7.5-7.66 8.95a1 1 0 0 1-.67-.01C7.5 20.5 4 18 4 13V6a1 1 0 0 1 1-1c2 0 4.5-1.2 6.24-2.72a1.17 1.17 0 0 1 1.52 0C14.51 3.81 17 5 19 5a1 1 0 0 1 1 1z"/><g transform="translate(7.5 6.5) scale(.375)" stroke-width="4"><rect width="18" height="11" x="3" y="11" rx="2" ry="2"/><path d="M7 11V7a5 5 0 0 1 10 0v4"/></g>',
|
|
45
|
+
'message-circle':'<path d="M7.9 20A9 9 0 1 0 4 16.1L2 22Z"/>', // bubble.left
|
|
46
|
+
power:'<path d="M12 2v10"/><path d="M18.4 6.6a9 9 0 1 1-12.77.04"/>',
|
|
47
|
+
'log-out':'<path d="M9 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h4"/><polyline points="16 17 21 12 16 7"/><line x1="21" x2="9" y1="12" y2="12"/>',
|
|
48
|
+
};
|
|
49
|
+
export const iconNames=Object.keys(paths);
|
|
50
|
+
export const icon=(name,size=16)=>{if(!paths[name])throw new Error(`Unknown icon ${name}`);return `<svg class="ui-icon" width="${size}" height="${size}" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">${paths[name]}</svg>`;};
|
|
51
|
+
// NSProgressIndicator's spinning style: eight spokes fading around, turned by CSS (static under reduced motion).
|
|
52
|
+
export const spinner=(size=14)=>`<svg class="ui-spinner" width="${size}" height="${size}" viewBox="0 0 24 24" aria-hidden="true">${[...Array(8)].map((_,i)=>`<line x1="12" y1="3" x2="12" y2="7.5" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" opacity="${(0.25+i*0.75/7).toFixed(2)}" transform="rotate(${i*45} 12 12)"/>`).join('')}</svg>`;
|
|
53
|
+
const appNames={claude:'Claude',codex:'Codex',t3:'T3 Code'};
|
|
54
|
+
// AgentAppIcon: the app's own icon. Claude's is its orange rounded tile with the official mark in white; Codex's is
|
|
55
|
+
// the official OpenAI app icon; T3 Code's comes from its installed .desktop entry. Tiles fill 82% of the frame, as a
|
|
56
|
+
// macOS app icon sits on its icon grid.
|
|
57
|
+
export function appMark(app,size=16,installed){
|
|
58
|
+
const label=appNames[app]||app;
|
|
59
|
+
if(app==='claude')return `<span class="app-mark claude" style="--mark:${size}px" role="img" aria-label="${label}"><i></i></span>`;
|
|
60
|
+
const src=app==='t3'?installed:app==='codex'?'assets/openai-mark.png':null;
|
|
61
|
+
if(!src)return '';
|
|
62
|
+
return `<span class="app-mark ${app}" style="--mark:${size}px" role="img" aria-label="${label}"><img src="${src}" alt=""></span>`;
|
|
63
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
<!doctype html><html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><meta http-equiv="Content-Security-Policy" content="default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data:; frame-src https:; connect-src 'none'; object-src 'none'; base-uri 'none'"><title>Shardflux</title><link rel="stylesheet" href="tokens.css"><link rel="stylesheet" href="style.css"></head><body><main id="app" aria-live="polite"></main><script type="module" src="app.mjs"></script></body></html>
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import {icon} from './icons.mjs';
|
|
2
|
+
import {escape as e} from './core.mjs';
|
|
3
|
+
export function deviceChoice(request,always){if(always&&request.outside)throw new Error('Access outside the conversation folder must be approved once.');return always?'always':'once';}
|
|
4
|
+
const preview=text=>String(text||'').split(/\s+/).filter(Boolean).join(' ').slice(0,280);
|
|
5
|
+
// NativeNotices.swift: one source for notification copy and actions. The first action is also the default (a click).
|
|
6
|
+
export function notificationFor(row){if(row.state==='sign-in')return {title:'Reconnect Claude',body:'Sign in once to continue your conversations.',actions:['settings','Sign in']};return {title:row.state==='done'?`${row.project} finished`:`${row.project} needs you`,subtitle:row.title,body:preview(row.last_message)||'Open the conversation to reply.',actions:[`open:${row.reference}`,'Open conversation']};}
|
|
7
|
+
export function deviceNotice(r){return {title:r.outside?'Review folder access':'Review computer access',subtitle:r.conversation_title||'',body:preview((r.outside?'Outside this conversation’s folder. ':'')+r.summary),actions:[`review:${r.request_id}`,'Review…',`deny:${r.request_id}`,'Deny']};}
|
|
8
|
+
export function credentialNotice(r){return {title:`Review ${new URL(r.site).host} sign-in`,subtitle:r.conversation_title||'',body:`Share ${r.fields.map(f=>f==='totp'?'a verification code':f).join(' and ')} from your chosen vault.`,actions:[`credential:${r.id}`,'Review…',`deny-credential:${r.id}`,'Deny']};}
|
|
9
|
+
export function safePlaceholder(file,root){return /^conv_[a-z0-9]{26}$/.test(file.conversation_id)&&file.path&&!file.path.startsWith('/')&&!file.path.split('/').some(p=>!p||p==='.'||p==='..')&&file.local_path?.startsWith(root+'/')&&!file.local_path.includes('/../')&&Number.isFinite(file.size)&&file.size>=0;}
|
|
10
|
+
export function installIntegrations({model,host,cli,render,button,registerActions,savePrefs}) {
|
|
11
|
+
const I={requests:[],credentials:[],files:{root:'',placeholders:[]},always:false,scope:'ask',remember:false,vault:'1password',item:'',loaded:false,checking:false,previous:new Map(),busy:false};model.integrations=I;
|
|
12
|
+
const prior=window.renderSurface;
|
|
13
|
+
function request(){return I.requests.find(r=>r.request_id===model.prefs.reviewDevice)||I.requests[0];}
|
|
14
|
+
function credential(){return I.credentials.find(r=>r.id===model.prefs.reviewCredential)||I.credentials[0];}
|
|
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','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
|
+
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
|
+
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
|
+
window.sfRefreshIntegrations=()=>refresh();
|
|
21
|
+
async function refresh(){if(I.checking)return;I.checking=true;try{const [requests,credentials,index,info]=await Promise.all([cli(['device','requests','--json']),cli(['credentials','list','--json']).catch(()=>({data:[]})),host('read',{name:'files.json'}),host('info')]);I.requests=requests.requests||[];I.credentials=(credentials.data||[]).filter(r=>r.state==='pending');I.files={root:index?.root||`${info.home}/Shardflux`,placeholders:(index?.placeholders||[]).filter(f=>safePlaceholder(f,index.root))};
|
|
22
|
+
if(model.prefs.notifyApprovals!==false)for(const r of I.requests)void host('notify',{key:`device:${r.request_id}`,...deviceNotice(r)}).catch(()=>{});
|
|
23
|
+
if(model.prefs.notifyApprovals!==false)for(const r of I.credentials)void host('notify',{key:`credential:${r.id}`,...credentialNotice(r)}).catch(()=>{});
|
|
24
|
+
if((model.view==='approvals'&&!I.requests.length)||(model.view==='credential'&&!I.credentials.length))await host('hide');I.loaded=true;if(['files','approvals','credential','tray'].includes(model.view))render();
|
|
25
|
+
}catch(err){if(['files','approvals'].includes(model.view)){model.error=err.message;render();}}finally{I.checking=false;}}
|
|
26
|
+
window.addEventListener('sf-view',ev=>{if(['files','approvals','credential'].includes(ev.detail))void refresh();});
|
|
27
|
+
document.querySelector('#app').addEventListener('change',ev=>{if(ev.target.name==='always-device')I.always=ev.target.checked;if(ev.target.name==='vault')I.vault=ev.target.value;if(ev.target.name==='credential-scope')I.scope=ev.target.value;if(ev.target.name==='credential-remember')I.remember=ev.target.checked;render();});
|
|
28
|
+
document.querySelector('#app').addEventListener('input',ev=>{if(ev.target.name==='vault-item'){I.item=ev.target.value;const b=document.querySelector('[data-action=allow-credential]');if(b)b.disabled=!I.item.trim();}});
|
|
29
|
+
async function approve(allow){const r=request();if(!r)throw new Error('The request is no longer waiting.');const always=allow&&deviceChoice(r,I.always)==='always';await cli(['device',allow?'approve':'deny',r.request_id,...(always?['--always']:[]),'--json']);I.always=false;await refresh();}
|
|
30
|
+
registerActions({'allow-device':()=>approve(true),'deny-device':()=>approve(false),'allow-credential':async()=>{const r=credential();if(!r)return;await cli(['credentials','approve',r.id,'--from',I.vault,'--item',I.item,'--scope',I.scope,...(I.remember?['--remember']:[]),'--json']);await refresh();},'deny-credential':async()=>{const r=credential();if(r)await cli(['credentials','deny',r.id,'--json']);await refresh();},'open-files':()=>host('open-folder',{path:I.files.root}),'update-now':async()=>{model.updating=true;model.prefs.updateStarted=Date.now();await savePrefs();await host('update');}});
|
|
31
|
+
registerActions({integration:async(a)=>{if(a.startsWith('review:')){model.prefs.reviewDevice=a.slice(7);await savePrefs();await host('view',{name:'approvals'});}else if(a.startsWith('credential:')){model.prefs.reviewCredential=a.slice(11);await savePrefs();await host('view',{name:'credential'});}else if(a.startsWith('always-credential:')){model.prefs.reviewCredential=a.slice(18);I.scope='project';await savePrefs();await host('view',{name:'credential'});}else if(a.startsWith('deny:')){await cli(['device','deny',a.slice(5),'--json']);await refresh();}else if(a.startsWith('deny-credential:')){await cli(['credentials','deny',a.slice(16),'--json']);await refresh();}else if(a.startsWith('download:')){const f=I.files.placeholders[Number(a.slice(9))];if(!f)return;const result=await cli(['files','pull',f.conversation_id,f.path,'--json']);if(!result.local_path)throw new Error('The file has not finished downloading.');await host('reveal',{path:result.local_path});await refresh();}}});
|
|
32
|
+
async function tick(){if(I.busy)return;I.busy=true;try{await refresh();const rows=model.rows||[];if(I.previous.size)for(const r of rows){const stamp=`${r.state}:${r.since}`,previous=I.previous.get(r.id);if(previous&&previous!==stamp&&(['needs-you','sign-in'].includes(r.state)&&model.prefs.notifyWaiting!==false||r.state==='done'&&model.prefs.notifyDone!==false))void host('notify',{key:`agent:${r.id}:${stamp}`,...notificationFor(r)}).catch(()=>{});}I.previous=new Map(rows.map(r=>[r.id,`${r.state}:${r.since}`]));}finally{I.busy=false;}}
|
|
33
|
+
setInterval(tick,4000);void tick();
|
|
34
|
+
setInterval(async()=>{try{if(await host('idle')){const status=await cli(['menubar','status','--json']);const info=await host('info');if(status.packaged_version&&status.packaged_version!==info.version)await host('refresh-app');}if(model.prefs.updateStarted){const update=await host('read',{name:'last-update.json'});if(update&&Date.parse(update.finished_at)>model.prefs.updateStarted){model.updating=false;delete model.prefs.updateStarted;await savePrefs();}}}catch{}},60000);
|
|
35
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import {icon,appMark,spinner} from './icons.mjs';
|
|
2
|
+
import {escape as e,cardState,shortcutLabel} from './core.mjs';
|
|
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)']};
|
|
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.'};
|
|
7
|
+
export function installSettings({model,host,cli,render,button,registerActions,refresh,savePrefs}) {
|
|
8
|
+
const S={pane:'General',info:{},identity:null,usage:null,spend:null,org:null,profiles:[],update:null,recording:false,confirmSignout:false};
|
|
9
|
+
model.settings=S;
|
|
10
|
+
const row=(title,detail,value)=>`<div class="setting-row"><div><b>${e(title)}</b><p>${e(detail)}</p></div><div>${value}</div></div>`;
|
|
11
|
+
const group=html=>`<section class="setting-group">${html}</section>`;
|
|
12
|
+
const toggle=(label,key,on)=>`<input type="checkbox" role="switch" aria-label="${e(label)}" data-pref="${key}" ${on?'checked':''}>`;
|
|
13
|
+
const link=(label,url)=>`<a href="${url}">${e(label)} ${icon('arrow-up-right',12)}</a>`;
|
|
14
|
+
function agents(){return `<div class="settings-cards">${['claude','codex',...(model.inspection?.connections?.some(c=>c.id==='t3')?['t3']:[])].map(id=>{const c=model.inspection?.connections?.find(c=>c.id===id),state=cardState(c),title={claude:'Claude',codex:'Codex',t3:'T3 Code'}[id];let targets=id==='claude'?['terminal','web']:['terminal'];if(model.launcherState?.targets?.t3?.installed)targets.push('t3');const open=model.launcherState?.targets?.open_in?.[id]||'terminal';return group(`<div class="connector ${state.kind}">${id==='t3'&&!model.appIcons?.t3?'<span class="app-mark missing-mark"></span>':appMark(id,36,model.appIcons?.[id])}<div><b>${title}</b><p class="fact">${e(state.fact)}</p></div><span class="status">${state.kind==='connected'?`${icon('circle-check-fill',14)} Connected`:state.button?button(e(state.button),`connect:${id}`,'small'):''}</span></div>${id==='t3'?row('Open a project','Use this project in T3 Code.',button('Choose project…','t3-project','small')):row('Open conversations in',`Where ${title} conversations open on this computer.`,`<select data-open-in="${id}" aria-label="Open ${title} conversations in">${targets.map(v=>`<option value="${v}" ${open===v?'selected':''}>${{terminal:'Terminal',web:'Browser (claude.ai)',t3:'T3 Code'}[v]}</option>`).join('')}</select>`)}${id==='claude'?row('Remote Control','Continue work in the Claude app.',state.kind==='connected'?'Connected':'Not connected'):''}`);}).join('')}</div>`;}
|
|
15
|
+
function body(){switch(S.pane){
|
|
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>`):''}`;}
|
|
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')));
|
|
18
|
+
case 'Agents':return agents();
|
|
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>`;
|
|
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')));
|
|
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>`;}
|
|
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')));
|
|
27
|
+
}}
|
|
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();}
|
|
35
|
+
window.addEventListener('sf-view',ev=>{if(ev.detail==='settings')void load();});
|
|
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();}});
|
|
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();}});
|
|
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();}}});
|
|
40
|
+
if(model.view==='settings')void load();
|
|
41
|
+
}
|