Save OpenAI API key in the keychain

This commit is contained in:
Antonio Scandurra 2023-06-02 12:15:25 +02:00
parent a81d164ea6
commit 3750e64d9f
3 changed files with 100 additions and 63 deletions

View file

@ -87,9 +87,7 @@
// Default width when the assistant is docked to the left or right. // Default width when the assistant is docked to the left or right.
"default_width": 450, "default_width": 450,
// Default height when the assistant is docked to the bottom. // Default height when the assistant is docked to the bottom.
"default_height": 320, "default_height": 320
// OpenAI API key.
"openai_api_key": null
}, },
// Whether the screen sharing icon is shown in the os status bar. // Whether the screen sharing icon is shown in the os status bar.
"show_call_status_icon": true, "show_call_status_icon": true,

View file

@ -16,7 +16,7 @@ use gpui::{
use isahc::{http::StatusCode, Request, RequestExt}; use isahc::{http::StatusCode, Request, RequestExt};
use language::{language_settings::SoftWrap, Buffer, LanguageRegistry}; use language::{language_settings::SoftWrap, Buffer, LanguageRegistry};
use settings::SettingsStore; use settings::SettingsStore;
use std::{io, sync::Arc}; use std::{cell::Cell, io, rc::Rc, sync::Arc};
use util::{post_inc, ResultExt, TryFutureExt}; use util::{post_inc, ResultExt, TryFutureExt};
use workspace::{ use workspace::{
dock::{DockPosition, Panel}, dock::{DockPosition, Panel},
@ -24,7 +24,12 @@ use workspace::{
pane, Pane, Workspace, pane, Pane, Workspace,
}; };
actions!(assistant, [NewContext, Assist, QuoteSelection, ToggleFocus]); const OPENAI_API_URL: &'static str = "https://api.openai.com/v1";
actions!(
assistant,
[NewContext, Assist, QuoteSelection, ToggleFocus, ResetKey]
);
pub fn init(cx: &mut AppContext) { pub fn init(cx: &mut AppContext) {
settings::register::<AssistantSettings>(cx); settings::register::<AssistantSettings>(cx);
@ -41,6 +46,7 @@ pub fn init(cx: &mut AppContext) {
cx.capture_action(AssistantEditor::cancel_last_assist); cx.capture_action(AssistantEditor::cancel_last_assist);
cx.add_action(AssistantEditor::quote_selection); cx.add_action(AssistantEditor::quote_selection);
cx.add_action(AssistantPanel::save_api_key); cx.add_action(AssistantPanel::save_api_key);
cx.add_action(AssistantPanel::reset_api_key);
} }
pub enum AssistantPanelEvent { pub enum AssistantPanelEvent {
@ -55,7 +61,9 @@ pub struct AssistantPanel {
width: Option<f32>, width: Option<f32>,
height: Option<f32>, height: Option<f32>,
pane: ViewHandle<Pane>, pane: ViewHandle<Pane>,
api_key_editor: ViewHandle<Editor>, api_key: Rc<Cell<Option<String>>>,
api_key_editor: Option<ViewHandle<Editor>>,
has_read_credentials: bool,
languages: Arc<LanguageRegistry>, languages: Arc<LanguageRegistry>,
fs: Arc<dyn Fs>, fs: Arc<dyn Fs>,
_subscriptions: Vec<Subscription>, _subscriptions: Vec<Subscription>,
@ -124,19 +132,12 @@ impl AssistantPanel {
.update(cx, |toolbar, cx| toolbar.add_item(buffer_search_bar, cx)); .update(cx, |toolbar, cx| toolbar.add_item(buffer_search_bar, cx));
pane pane
}); });
let mut this = Self { let mut this = Self {
pane, pane,
api_key_editor: cx.add_view(|cx| { api_key: Rc::new(Cell::new(None)),
let mut editor = Editor::single_line( api_key_editor: None,
Some(Arc::new(|theme| theme.assistant.api_key_editor.clone())), has_read_credentials: false,
cx,
);
editor.set_placeholder_text(
"sk-000000000000000000000000000000000000000000000000",
cx,
);
editor
}),
languages: workspace.app_state().languages.clone(), languages: workspace.app_state().languages.clone(),
fs: workspace.app_state().fs.clone(), fs: workspace.app_state().fs.clone(),
width: None, width: None,
@ -145,9 +146,6 @@ impl AssistantPanel {
}; };
let mut old_dock_position = this.position(cx); let mut old_dock_position = this.position(cx);
let mut old_openai_api_key = settings::get::<AssistantSettings>(cx)
.openai_api_key
.clone();
this._subscriptions = vec![ this._subscriptions = vec![
cx.observe(&this.pane, |_, _, cx| cx.notify()), cx.observe(&this.pane, |_, _, cx| cx.notify()),
cx.subscribe(&this.pane, Self::handle_pane_event), cx.subscribe(&this.pane, Self::handle_pane_event),
@ -157,17 +155,6 @@ impl AssistantPanel {
old_dock_position = new_dock_position; old_dock_position = new_dock_position;
cx.emit(AssistantPanelEvent::DockPositionChanged); cx.emit(AssistantPanelEvent::DockPositionChanged);
} }
let new_openai_api_key = settings::get::<AssistantSettings>(cx)
.openai_api_key
.clone();
if old_openai_api_key != new_openai_api_key {
old_openai_api_key = new_openai_api_key;
if this.has_focus(cx) {
cx.focus_self();
}
cx.notify();
}
}), }),
]; ];
@ -194,22 +181,49 @@ impl AssistantPanel {
fn add_context(&mut self, cx: &mut ViewContext<Self>) { fn add_context(&mut self, cx: &mut ViewContext<Self>) {
let focus = self.has_focus(cx); let focus = self.has_focus(cx);
let editor = cx.add_view(|cx| AssistantEditor::new(self.languages.clone(), cx)); let editor = cx
.add_view(|cx| AssistantEditor::new(self.api_key.clone(), self.languages.clone(), cx));
self.pane.update(cx, |pane, cx| { self.pane.update(cx, |pane, cx| {
pane.add_item(Box::new(editor), true, focus, None, cx) pane.add_item(Box::new(editor), true, focus, None, cx)
}); });
} }
fn save_api_key(&mut self, _: &menu::Confirm, cx: &mut ViewContext<Self>) { fn save_api_key(&mut self, _: &menu::Confirm, cx: &mut ViewContext<Self>) {
let api_key = self.api_key_editor.read(cx).text(cx); if let Some(api_key) = self
if !api_key.is_empty() { .api_key_editor
settings::update_settings_file::<AssistantSettings>( .as_ref()
self.fs.clone(), .map(|editor| editor.read(cx).text(cx))
cx, {
move |settings| settings.openai_api_key = Some(api_key), if !api_key.is_empty() {
); cx.platform()
.write_credentials(OPENAI_API_URL, "Bearer", api_key.as_bytes())
.log_err();
self.api_key.set(Some(api_key));
self.api_key_editor.take();
cx.focus_self();
cx.notify();
}
} }
} }
fn reset_api_key(&mut self, _: &ResetKey, cx: &mut ViewContext<Self>) {
cx.platform().delete_credentials(OPENAI_API_URL).log_err();
self.api_key.take();
self.api_key_editor = Some(build_api_key_editor(cx));
cx.focus_self();
cx.notify();
}
}
fn build_api_key_editor(cx: &mut ViewContext<AssistantPanel>) -> ViewHandle<Editor> {
cx.add_view(|cx| {
let mut editor = Editor::single_line(
Some(Arc::new(|theme| theme.assistant.api_key_editor.clone())),
cx,
);
editor.set_placeholder_text("sk-000000000000000000000000000000000000000000000000", cx);
editor
})
} }
impl Entity for AssistantPanel { impl Entity for AssistantPanel {
@ -223,10 +237,7 @@ impl View for AssistantPanel {
fn render(&mut self, cx: &mut ViewContext<Self>) -> AnyElement<Self> { fn render(&mut self, cx: &mut ViewContext<Self>) -> AnyElement<Self> {
let style = &theme::current(cx).assistant; let style = &theme::current(cx).assistant;
if settings::get::<AssistantSettings>(cx) if let Some(api_key_editor) = self.api_key_editor.as_ref() {
.openai_api_key
.is_none()
{
Flex::column() Flex::column()
.with_child( .with_child(
Text::new( Text::new(
@ -236,7 +247,7 @@ impl View for AssistantPanel {
.aligned(), .aligned(),
) )
.with_child( .with_child(
ChildView::new(&self.api_key_editor, cx) ChildView::new(api_key_editor, cx)
.contained() .contained()
.with_style(style.api_key_editor.container) .with_style(style.api_key_editor.container)
.aligned(), .aligned(),
@ -252,13 +263,10 @@ impl View for AssistantPanel {
fn focus_in(&mut self, _: gpui::AnyViewHandle, cx: &mut ViewContext<Self>) { fn focus_in(&mut self, _: gpui::AnyViewHandle, cx: &mut ViewContext<Self>) {
if cx.is_self_focused() { if cx.is_self_focused() {
if settings::get::<AssistantSettings>(cx) if let Some(api_key_editor) = self.api_key_editor.as_ref() {
.openai_api_key cx.focus(api_key_editor);
.is_some()
{
cx.focus(&self.pane);
} else { } else {
cx.focus(&self.api_key_editor); cx.focus(&self.pane);
} }
} }
} }
@ -323,8 +331,30 @@ impl Panel for AssistantPanel {
} }
fn set_active(&mut self, active: bool, cx: &mut ViewContext<Self>) { fn set_active(&mut self, active: bool, cx: &mut ViewContext<Self>) {
if active && self.pane.read(cx).items_len() == 0 { if active {
self.add_context(cx); if self.api_key.clone().take().is_none() && !self.has_read_credentials {
self.has_read_credentials = true;
let api_key = if let Some((_, api_key)) = cx
.platform()
.read_credentials(OPENAI_API_URL)
.log_err()
.flatten()
{
String::from_utf8(api_key).log_err()
} else {
None
};
if let Some(api_key) = api_key {
self.api_key.set(Some(api_key));
} else if self.api_key_editor.is_none() {
self.api_key_editor = Some(build_api_key_editor(cx));
cx.notify();
}
}
if self.pane.read(cx).items_len() == 0 {
self.add_context(cx);
}
} }
} }
@ -349,7 +379,11 @@ impl Panel for AssistantPanel {
} }
fn has_focus(&self, cx: &WindowContext) -> bool { fn has_focus(&self, cx: &WindowContext) -> bool {
self.pane.read(cx).has_focus() || self.api_key_editor.is_focused(cx) self.pane.read(cx).has_focus()
|| self
.api_key_editor
.as_ref()
.map_or(false, |editor| editor.is_focused(cx))
} }
fn is_focus_event(event: &Self::Event) -> bool { fn is_focus_event(event: &Self::Event) -> bool {
@ -364,6 +398,7 @@ struct Assistant {
completion_count: usize, completion_count: usize,
pending_completions: Vec<PendingCompletion>, pending_completions: Vec<PendingCompletion>,
languages: Arc<LanguageRegistry>, languages: Arc<LanguageRegistry>,
api_key: Rc<Cell<Option<String>>>,
} }
impl Entity for Assistant { impl Entity for Assistant {
@ -371,7 +406,11 @@ impl Entity for Assistant {
} }
impl Assistant { impl Assistant {
fn new(language_registry: Arc<LanguageRegistry>, cx: &mut ModelContext<Self>) -> Self { fn new(
api_key: Rc<Cell<Option<String>>>,
language_registry: Arc<LanguageRegistry>,
cx: &mut ModelContext<Self>,
) -> Self {
let mut this = Self { let mut this = Self {
buffer: cx.add_model(|_| MultiBuffer::new(0)), buffer: cx.add_model(|_| MultiBuffer::new(0)),
messages: Default::default(), messages: Default::default(),
@ -379,6 +418,7 @@ impl Assistant {
completion_count: Default::default(), completion_count: Default::default(),
pending_completions: Default::default(), pending_completions: Default::default(),
languages: language_registry, languages: language_registry,
api_key,
}; };
this.push_message(Role::User, cx); this.push_message(Role::User, cx);
this this
@ -399,10 +439,7 @@ impl Assistant {
stream: true, stream: true,
}; };
if let Some(api_key) = settings::get::<AssistantSettings>(cx) if let Some(api_key) = self.api_key.clone().take() {
.openai_api_key
.clone()
{
let stream = stream_completion(api_key, cx.background().clone(), request); let stream = stream_completion(api_key, cx.background().clone(), request);
let response = self.push_message(Role::Assistant, cx); let response = self.push_message(Role::Assistant, cx);
self.push_message(Role::User, cx); self.push_message(Role::User, cx);
@ -496,8 +533,12 @@ struct AssistantEditor {
} }
impl AssistantEditor { impl AssistantEditor {
fn new(language_registry: Arc<LanguageRegistry>, cx: &mut ViewContext<Self>) -> Self { fn new(
let assistant = cx.add_model(|cx| Assistant::new(language_registry, cx)); api_key: Rc<Cell<Option<String>>>,
language_registry: Arc<LanguageRegistry>,
cx: &mut ViewContext<Self>,
) -> Self {
let assistant = cx.add_model(|cx| Assistant::new(api_key, language_registry, cx));
let editor = cx.add_view(|cx| { let editor = cx.add_view(|cx| {
let mut editor = Editor::for_multibuffer(assistant.read(cx).buffer.clone(), None, cx); let mut editor = Editor::for_multibuffer(assistant.read(cx).buffer.clone(), None, cx);
editor.set_soft_wrap_mode(SoftWrap::EditorWidth, cx); editor.set_soft_wrap_mode(SoftWrap::EditorWidth, cx);
@ -685,7 +726,7 @@ async fn stream_completion(
let (tx, rx) = futures::channel::mpsc::unbounded::<Result<OpenAIResponseStreamEvent>>(); let (tx, rx) = futures::channel::mpsc::unbounded::<Result<OpenAIResponseStreamEvent>>();
let json_data = serde_json::to_string(&request)?; let json_data = serde_json::to_string(&request)?;
let mut response = Request::post("https://api.openai.com/v1/chat/completions") let mut response = Request::post(format!("{OPENAI_API_URL}/chat/completions"))
.header("Content-Type", "application/json") .header("Content-Type", "application/json")
.header("Authorization", format!("Bearer {}", api_key)) .header("Authorization", format!("Bearer {}", api_key))
.body(json_data)? .body(json_data)?

View file

@ -16,7 +16,6 @@ pub struct AssistantSettings {
pub dock: AssistantDockPosition, pub dock: AssistantDockPosition,
pub default_width: f32, pub default_width: f32,
pub default_height: f32, pub default_height: f32,
pub openai_api_key: Option<String>,
} }
#[derive(Clone, Default, Serialize, Deserialize, JsonSchema, Debug)] #[derive(Clone, Default, Serialize, Deserialize, JsonSchema, Debug)]
@ -24,7 +23,6 @@ pub struct AssistantSettingsContent {
pub dock: Option<AssistantDockPosition>, pub dock: Option<AssistantDockPosition>,
pub default_width: Option<f32>, pub default_width: Option<f32>,
pub default_height: Option<f32>, pub default_height: Option<f32>,
pub openai_api_key: Option<String>,
} }
impl Setting for AssistantSettings { impl Setting for AssistantSettings {