35 Commits
1.1.0 ... 1.1.1

Author SHA1 Message Date
Jeffry Samuel
a7b6e6bbce Update flatpak-builder.yml 2024-08-12 23:15:53 -06:00
aritra saha
801c10fb77 Update hi.po (#240) 2024-08-12 22:58:04 -06:00
aritra saha
50520b8474 Update bn.po (#239) 2024-08-12 22:53:43 -06:00
jeffser
b66e2102d3 Spanish update 2024-08-12 22:21:32 -06:00
jeffser
8c0f1fd4d5 Updated languages 2024-08-12 22:17:35 -06:00
jeffser
8b851d1b56 Preparing for 1.1.1 2024-08-12 22:16:47 -06:00
jeffser
f36d6e1b29 Removed bugged imported chats 2024-08-12 22:12:09 -06:00
jeffser
eecac162ef Update translations 2024-08-12 22:03:47 -06:00
jeffser
82e7a3a9e1 Better caption names on dropdown 2024-08-12 18:57:17 -06:00
jeffser
f0505a0242 Only enable youtube caption search when there's more than 10 captions 2024-08-12 18:55:06 -06:00
jeffser
11dd13b430 Added ollama debbugging messages to about dialog 2024-08-11 22:30:56 -06:00
jeffser
b8fe222052 Added duplicate chat option 2024-08-11 22:11:17 -06:00
jeffser
47d19a58aa Give message focus when editing it 2024-08-11 21:50:50 -06:00
jeffser
fd67afbf33 Fixed message editor 2024-08-11 21:48:31 -06:00
jeffser
d06e08a64e Fix message regeneration 2024-08-11 21:36:31 -06:00
jeffser
77b08d9e52 Added loading spinner to regenerating message 2024-08-11 21:15:26 -06:00
jeffser
9451bf88d0 Removed french language set 2024-08-11 16:02:30 -06:00
jeffser
82bb50d663 Merge branch 'main' of github.com-jeffser:Jeffser/Alpaca 2024-08-11 16:01:14 -06:00
jeffser
edc3053774 Focus message entry when creating a new chat 2024-08-11 15:59:39 -06:00
Louis Chauvet-Villaret
1320ddb7d4 Hola Jeffry, traduje la aplicacion, buen dia (#236) 2024-08-11 15:37:51 -06:00
jeffser
d95f06a230 Make buttons visible when stopping message 2024-08-11 15:08:26 -06:00
jeffser
938ace91c1 Fixed import chat 2024-08-11 14:55:18 -06:00
jeffser
175cfad81c Welcome screen appears when clearing chat 2024-08-11 14:49:12 -06:00
jeffser
2f399dbb64 Removed bold from manage button label 2024-08-11 14:44:19 -06:00
jeffser
27558b85af Added top margin to model selector 2024-08-11 14:23:30 -06:00
jeffser
bcc1f3fa65 Changed 'Open Model Manager' to pill button on welcome screen 2024-08-11 14:19:00 -06:00
jeffser
fd92a86c5e CTRL+W and CTRL+Q stops local instance before closing the app 2024-08-11 13:23:34 -06:00
jeffser
3b95d369b8 Added Azerbaijani template 2024-08-11 01:45:57 -06:00
Jeffry Samuel
a12920d801 Update SECURITY.md 2024-08-11 00:47:58 -06:00
Jeffry Samuel
2dd63df533 Create SECURITY.md 2024-08-11 00:44:49 -06:00
Jeffry Samuel
cea1aa5028 Update CODE_OF_CONDUCT.md 2024-08-11 00:40:27 -06:00
Jeffry Samuel
54b96d4e3a Update CODE_OF_CONDUCT.md 2024-08-11 00:40:04 -06:00
Jeffry Samuel
a470136476 Create CODE_OF_CONDUCT.md 2024-08-11 00:37:44 -06:00
Jeffry Samuel
7d35cb08dd Create CONTRIBUTING.md 2024-08-11 00:32:55 -06:00
aritra saha
1f03f1032e Update bn.po and hipo (#223)
* Update bn.po

* Update hi.po
2024-08-11 00:14:48 -06:00
22 changed files with 6475 additions and 3548 deletions

View File

@@ -13,6 +13,6 @@ jobs:
- uses: actions/checkout@v4
- uses: flatpak/flatpak-github-actions/flatpak-builder@v6
with:
bundle: Alpaca.flatpak
bundle: com.jeffser.Alpaca.flatpak
manifest-path: com.jeffser.Alpaca.json
cache-key: flatpak-builder-${{ github.sha }}

4
CODE_OF_CONDUCT.md Normal file
View File

@@ -0,0 +1,4 @@
Alpaca follows [GNOME's code of conduct](https://conduct.gnome.org/), please make sure to read it before interacting in any way with this repository.
To report any misconduct please reach out via private message on
- X (formally Twitter): [@jeffrysamuer](https://x.com/jeffrysamuer)
- Mastodon: [@jeffser@floss.social](https://floss.social/@jeffser)

30
CONTRIBUTING.md Normal file
View File

@@ -0,0 +1,30 @@
# Contributing Rules
## Translations
If you want to translate or contribute on existing translations please read [this discussion](https://github.com/Jeffser/Alpaca/discussions/153).
## Code
1) Before contributing code make sure there's an open [issue](https://github.com/Jeffser/Alpaca/issues) for that particular problem or feature.
2) Ask to contribute on the responses to the issue.
3) Wait for [my](https://github.com/Jeffser) approval, I might have already started working on that issue.
4) Test your code before submitting a pull request.
## Q&A
### Do I need to comment my code?
There's no need to add comments if the code is easy to read by itself.
### What if I need help or I don't understand the existing code?
You can reach out on the issue, I'll try to answer as soon as possible.
### What IDE should I use?
I use Gnome Builder but you can use whatever you want.
### Can I be credited?
You might be credited on the GitHub repository in the [thanks](https://github.com/Jeffser/Alpaca/blob/main/README.md#thanks) section of the README.

12
SECURITY.md Normal file
View File

@@ -0,0 +1,12 @@
# Security Policy
## Supported Packaging
Alpaca only supports [Flatpak](https://flatpak.org/) packaging officially, any other packaging methods might not behave as expected.
## Official Versions
The only ways Alpaca is being distributed officially are:
- [Alpaca's GitHub Repository Releases Page](https://github.com/Jeffser/Alpaca/releases)
- [Flathub](https://flathub.org/apps/com.jeffser.Alpaca)

View File

@@ -78,6 +78,28 @@
<url type="contribute">https://github.com/Jeffser/Alpaca/discussions/154</url>
<url type="vcs-browser">https://github.com/Jeffser/Alpaca</url>
<releases>
<release version="1.1.1" date="2024-08-12">
<url type="details">https://github.com/Jeffser/Alpaca/releases/tag/1.1.1</url>
<description>
<p>New</p>
<ul>
<li>New duplicate chat option</li>
<li>Changed model selector appearance</li>
<li>Message entry is focused on launch and chat change</li>
<li>Message is focused when it's being edited</li>
<li>Added loading spinner when regenerating a message</li>
<li>Added Ollama debugging to 'About Alpaca' dialog</li>
<li>Changed YouTube transcription dialog appearance and behavior</li>
</ul>
<p>Fixes</p>
<ul>
<li>CTRL+W and CTRL+Q stops local instance before closing the app</li>
<li>Changed appearance of 'Open Model Manager' button on welcome screen</li>
<li>Fixed message generation not working consistently</li>
<li>Fixed message edition not working consistently</li>
</ul>
</description>
</release>
<release version="1.1.0" date="2024-08-10">
<url type="details">https://github.com/Jeffser/Alpaca/releases/tag/1.1.0</url>
<description>

View File

@@ -1,5 +1,5 @@
project('Alpaca', 'c',
version: '1.1.0',
version: '1.1.1',
meson_version: '>= 0.62.0',
default_options: [ 'warning_level=2', 'werror=false', ],
)

File diff suppressed because it is too large Load Diff

2306
po/az.po Normal file

File diff suppressed because it is too large Load Diff

856
po/bn.po

File diff suppressed because it is too large Load Diff

837
po/es.po

File diff suppressed because it is too large Load Diff

892
po/fr.po

File diff suppressed because it is too large Load Diff

855
po/hi.po

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

820
po/ru.po

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -330,7 +330,7 @@ def youtube_caption_response(self, dialog, task, video_url, caption_drop_down):
yt = YouTube(video_url)
text = "{}\n{}\n{}\n\n".format(yt.title, yt.author, yt.watch_url)
selected_caption = caption_drop_down.get_selected_item().get_string()
for event in yt.captions[selected_caption.split(' | ')[1]].json_captions['events']:
for event in yt.captions[selected_caption.split('(')[1][:-1]].json_captions['events']:
text += "{}\n".format(event['segs'][0]['utf8'].replace('\n', '\\n'))
if not os.path.exists(os.path.join(self.cache_dir, 'tmp/youtube')):
os.makedirs(os.path.join(self.cache_dir, 'tmp/youtube'))
@@ -348,9 +348,9 @@ def youtube_caption(self, video_url):
return
caption_list = Gtk.StringList()
for caption in captions:
caption_list.append("{} | {}".format(caption.name, caption.code))
caption_list.append("{} ({})".format(caption.name.title(), caption.code))
caption_drop_down = Gtk.DropDown(
enable_search=True,
enable_search=len(captions) > 10,
model=caption_list
)
dialog = Adw.AlertDialog(

View File

@@ -3,6 +3,7 @@
Handles running, stopping and resetting the integrated Ollama instance
"""
import subprocess
import threading
import os
from time import sleep
from logging import getLogger
@@ -15,6 +16,14 @@ instance = None
port = 11435
overrides = {}
def log_output(pipe):
with open(os.path.join(data_dir, 'tmp.log'), 'a') as f:
with pipe:
for line in iter(pipe.readline, ''):
print(line, end='')
f.write(line)
f.flush()
def start():
if not os.path.isdir(os.path.join(cache_dir, 'tmp/ollama')):
os.mkdir(os.path.join(cache_dir, 'tmp/ollama'))
@@ -23,7 +32,9 @@ def start():
params["OLLAMA_HOST"] = f"127.0.0.1:{port}" # You can't change this directly sorry :3
params["HOME"] = data_dir
params["TMPDIR"] = os.path.join(cache_dir, 'tmp/ollama')
instance = subprocess.Popen(["ollama", "serve"], env={**os.environ, **params}, stderr=subprocess.PIPE, text=True)
instance = subprocess.Popen(["ollama", "serve"], env={**os.environ, **params}, stderr=subprocess.PIPE, stdout=subprocess.PIPE, text=True)
threading.Thread(target=log_output, args=(instance.stdout,)).start()
threading.Thread(target=log_output, args=(instance.stderr,)).start()
logger.info("Starting Alpaca's Ollama instance...")
logger.debug(params)
sleep(1)

View File

@@ -53,7 +53,7 @@ class AlpacaApplication(Adw.Application):
def __init__(self, version):
super().__init__(application_id='com.jeffser.Alpaca',
flags=Gio.ApplicationFlags.DEFAULT_FLAGS)
self.create_action('quit', lambda *_: self.quit(), ['<primary>w', '<primary>q'])
self.create_action('quit', lambda *_: self.props.active_window.closing_app(None), ['<primary>w', '<primary>q'])
self.create_action('preferences', lambda *_: AlpacaWindow.show_preferences_dialog(self.props.active_window), ['<primary>comma'])
self.create_action('about', self.on_about_action)
self.version = version

View File

@@ -13,6 +13,7 @@
}
.manage_models_button {
padding: 6px 8px 6px 8px;
font-weight: 400;
}
.model_list_box > * {
margin: 0;
@@ -24,3 +25,6 @@
.user_message:focus, .response_message:focus, .editing_message_textview:focus, .code_block:focus {
box-shadow: 0 0 1px 2px mix(@accent_color, @window_bg_color, 0.5);
}
.model_popover {
margin-top: 6px;
}

View File

@@ -165,12 +165,13 @@ class AlpacaWindow(Adw.ApplicationWindow):
@Gtk.Template.Callback()
def stop_message(self, button=None):
if self.loading_spinner:
self.chat_container.remove(self.loading_spinner)
self.loading_spinner.get_parent().remove(self.loading_spinner)
message_id = list(self.chats["chats"][self.chats["selected_chat"]]["messages"])[-1]
formated_date = GLib.markup_escape_text(self.generate_datetime_format(datetime.strptime(self.chats["chats"][self.chats["selected_chat"]]["messages"][message_id]["date"], '%Y/%m/%d %H:%M:%S')))
text = f"\n\n{self.convert_model_name(self.chats['chats'][self.chats['selected_chat']]['messages'][message_id]['model'], 0)}\n<small>{formated_date}</small>"
self.bot_message.insert_markup(self.bot_message.get_end_iter(), text, len(text.encode('utf-8')))
self.add_code_blocks()
self.bot_message_button_container.set_visible(True)
self.toggle_ui_sensitive(True)
self.switch_send_stop_button(True)
self.bot_message = None
@@ -183,7 +184,7 @@ class AlpacaWindow(Adw.ApplicationWindow):
def send_message(self, button=None):
if self.editing_message:
self.editing_message["button_container"].set_visible(True)
self.editing_message["text_view"].set_css_classes(["flat"])
self.editing_message["text_view"].set_css_classes(["flat", "user_message"])
self.editing_message["text_view"].set_cursor_visible(False)
self.editing_message["text_view"].set_editable(False)
buffer = self.editing_message["text_view"].get_buffer()
@@ -345,6 +346,7 @@ class AlpacaWindow(Adw.ApplicationWindow):
else:
logger.info("Closing app...")
local_instance.stop()
self.get_application().quit()
@Gtk.Template.Callback()
def model_spin_changed(self, spin):
@@ -590,6 +592,13 @@ class AlpacaWindow(Adw.ApplicationWindow):
text_view.set_cursor_visible(True)
self.editing_message = {"text_view": text_view, "id": message_id, "button_container": button_container, "footer": footer}
if text_view.observe_controllers().get_n_items() == 8:
print(text_view.observe_controllers().get_n_items())
enter_key_controller = Gtk.EventControllerKey.new()
enter_key_controller.connect("key-pressed", lambda controller, keyval, keycode, state: self.handle_enter_key() if keyval==Gdk.KEY_Return and not (state & Gdk.ModifierType.SHIFT_MASK) else None)
text_view.add_controller(enter_key_controller)
print(text_view.observe_controllers().get_n_items())
self.set_focus(text_view)
def preview_file(self, file_path, file_type, presend_name):
logger.debug(f"Previewing file: {file_path}")
@@ -1054,7 +1063,7 @@ Generate a title following these rules:
if self.bot_message is None:
sys.exit()
vadjustment = self.chat_window.get_vadjustment()
if message_id not in self.chats["chats"][self.chats["selected_chat"]]["messages"] or vadjustment.get_value() + 50 >= vadjustment.get_upper() - vadjustment.get_page_size():
if not self.chats["chats"][self.chats["selected_chat"]]["messages"][message_id] or vadjustment.get_value() + 50 >= vadjustment.get_upper() - vadjustment.get_page_size():
GLib.idle_add(vadjustment.set_value, vadjustment.get_upper())
if 'done' in data and data['done']:
formated_date = GLib.markup_escape_text(self.generate_datetime_format(datetime.strptime(self.chats["chats"][self.chats["selected_chat"]]["messages"][message_id]["date"], '%Y/%m/%d %H:%M:%S')))
@@ -1066,8 +1075,8 @@ Generate a title following these rules:
first_paragraph = self.bot_message.get_text(self.bot_message.get_start_iter(), self.bot_message.get_end_iter(), False).split("\n")[0]
GLib.idle_add(self.show_notification, self.chats["selected_chat"], first_paragraph[:100] + (first_paragraph[100:] and '...'), Gio.ThemedIcon.new("chat-message-new-symbolic"))
else:
if not self.chats["chats"][self.chats["selected_chat"]]["messages"][message_id]["content"] and self.loading_spinner:
GLib.idle_add(self.chat_container.remove, self.loading_spinner)
if self.loading_spinner:
GLib.idle_add(self.loading_spinner.get_parent().remove, self.loading_spinner)
self.loading_spinner = None
GLib.idle_add(self.bot_message.insert, self.bot_message.get_end_iter(), data['message']['content'])
self.chats["chats"][self.chats["selected_chat"]]["messages"][message_id]['content'] += data['message']['content']
@@ -1083,12 +1092,13 @@ Generate a title following these rules:
def run_message(self, messages, model, message_id):
logger.debug("Running message")
self.bot_message_button_container.set_visible(False)
self.chats["chats"][self.chats["selected_chat"]]["messages"][message_id] = {
"role": "assistant",
"model": model,
"date": datetime.now().strftime("%Y/%m/%d %H:%M:%S"),
"content": ''
}
if message_id not in self.chats["chats"][self.chats["selected_chat"]]["messages"]:
self.chats["chats"][self.chats["selected_chat"]]["messages"][message_id] = {
"role": "assistant",
"model": model,
"date": datetime.now().strftime("%Y/%m/%d %H:%M:%S"),
"content": ''
}
if self.regenerate_button:
GLib.idle_add(self.chat_container.remove, self.regenerate_button)
try:
@@ -1112,7 +1122,7 @@ Generate a title following these rules:
GLib.idle_add(self.switch_send_stop_button, True)
GLib.idle_add(self.toggle_ui_sensitive, True)
if self.loading_spinner:
GLib.idle_add(self.chat_container.remove, self.loading_spinner)
GLib.idle_add(self.loading_spinner.get_parent().remove, self.loading_spinner)
self.loading_spinner = None
def regenerate_message(self, message_id, bot_message_box, bot_message_button_container):
@@ -1131,10 +1141,12 @@ Generate a title following these rules:
self.bot_message_box = bot_message_box
for widget in list(bot_message_box):
bot_message_box.remove(widget)
self.loading_spinner = Gtk.Spinner(spinning=True, margin_top=12, margin_bottom=12, hexpand=True)
bot_message_box.append(self.loading_spinner)
bot_message_box.append(self.bot_message_view)
history = self.convert_history_to_ollama()[:list(self.chats["chats"][self.chats["selected_chat"]]["messages"].keys()).index(message_id)]
if message_id in self.chats["chats"][self.chats["selected_chat"]]["messages"]:
del self.chats["chats"][self.chats["selected_chat"]]["messages"][message_id]
self.chats["chats"][self.chats["selected_chat"]]["messages"][message_id]['content'] = ''
history = self.convert_history_to_ollama()[:list(self.chats["chats"][self.chats["selected_chat"]]["messages"].keys()).index(message_id)]
data = {
"model": self.get_current_model(1),
"messages": history,
@@ -1329,7 +1341,7 @@ Generate a title following these rules:
button = Gtk.Button(
label=_("Open Model Manager"),
tooltip_text=_("Open Model Manager"),
css_classes=["accent"]
css_classes=["accent", "pill"]
)
button.connect('clicked', lambda *_ : self.manage_models_dialog.present(self))
button_container.append(button)
@@ -1358,12 +1370,16 @@ Generate a title following these rules:
for chat_name in self.chats["chats"].keys():
self.chats["order"].append(chat_name)
self.model_list_box.select_row(self.model_list_box.get_row_at_index(0))
self.chats["chats"] = {key: value for key, value in self.chats["chats"].items() if key in self.chats["order"]}
if self.chats["selected_chat"] not in self.chats["order"]:
self.chats["selected_chat"] = self.chats["order"][0]
if len(self.chats["chats"][self.chats["selected_chat"]]["messages"].keys()) > 0:
last_model_used = self.chats["chats"][self.chats["selected_chat"]]["messages"][list(self.chats["chats"][self.chats["selected_chat"]]["messages"].keys())[-1]]["model"]
for i, m in enumerate(self.local_models):
if m == last_model_used:
self.model_list_box.select_row(self.model_list_box.get_row_at_index(i))
break
except Exception as e:
logger.error(e)
self.chats = {"chats": {}, "selected_chat": None, "order": []}
@@ -1395,6 +1411,7 @@ Generate a title following these rules:
for widget in list(self.chat_container): self.chat_container.remove(widget)
self.chats["chats"][self.chats["selected_chat"]]["messages"] = {}
self.save_history()
self.load_history_into_chat()
def delete_chat(self, chat_name):
logger.info("Deleting chat")
@@ -1409,6 +1426,14 @@ Generate a title following these rules:
if self.chats['selected_chat'] == chat_name:
self.chat_list_box.select_row(self.chat_list_box.get_row_at_index(0))
def duplicate_chat(self, chat_name):
new_chat_name = self.generate_numbered_name(_("Copy of {}").format(chat_name), self.chats["chats"].keys())
self.chats["chats"][new_chat_name] = self.chats["chats"][chat_name]
self.chats["order"].insert(0, new_chat_name)
self.save_history()
self.new_chat_element(new_chat_name, True, False)
shutil.copytree(os.path.join(self.data_dir, "chats", chat_name), os.path.join(self.data_dir, "chats", new_chat_name))
def rename_chat(self, old_chat_name, new_chat_name, label_element):
logger.info(f"Renaming chat \"{old_chat_name}\" -> \"{new_chat_name}\"")
new_chat_name = self.generate_numbered_name(new_chat_name, self.chats["chats"].keys())
@@ -1430,6 +1455,7 @@ Generate a title following these rules:
self.chats["order"].insert(0, chat_name)
self.save_history()
self.new_chat_element(chat_name, True, False)
self.set_focus(self.message_text_view)
def stop_pull_model(self, model_name):
logger.debug("Stopping model pull")
@@ -1453,7 +1479,7 @@ Generate a title following these rules:
menu_model=self.chat_right_click_menu,
has_arrow=False,
halign=1,
height_request=125
height_request=155
)
self.selected_chat_row = chat_row
position = Gdk.Rectangle()
@@ -1613,6 +1639,7 @@ Generate a title following these rules:
for chat_name, chat_content in data.items():
new_chat_name = self.generate_numbered_name(chat_name, list(self.chats['chats'].keys()))
self.chats['chats'][new_chat_name] = chat_content
self.chats["order"].insert(0, new_chat_name)
src_path = os.path.join(temp_dir, chat_name)
if os.path.exists(src_path) and os.path.isdir(src_path):
dest_path = os.path.join(self.data_dir, "chats", new_chat_name)
@@ -1710,6 +1737,8 @@ Generate a title following these rules:
action_name = action.get_name()
if action_name in ('delete_chat', 'delete_current_chat'):
dialogs.delete_chat(self, chat_name)
elif action_name in ('duplicate_chat', 'duplicate_current_chat'):
self.duplicate_chat(chat_name)
elif action_name in ('rename_chat', 'rename_current_chat'):
dialogs.rename_chat(self, chat_name, chat_row.get_child())
elif action_name in ('export_chat', 'export_current_chat'):
@@ -1778,9 +1807,9 @@ Generate a title following these rules:
self.available_models = json.load(f)
if not os.path.exists(os.path.join(self.data_dir, "chats")):
os.makedirs(os.path.join(self.data_dir, "chats"))
key_controller = Gtk.EventControllerKey.new()
key_controller.connect("key-pressed", lambda controller, keyval, keycode, state: self.handle_enter_key() if keyval==Gdk.KEY_Return and not (state & Gdk.ModifierType.SHIFT_MASK) else None)
self.message_text_view.add_controller(key_controller)
enter_key_controller = Gtk.EventControllerKey.new()
enter_key_controller.connect("key-pressed", lambda controller, keyval, keycode, state: self.handle_enter_key() if keyval==Gdk.KEY_Return and not (state & Gdk.ModifierType.SHIFT_MASK) else None)
self.message_text_view.add_controller(enter_key_controller)
self.set_help_overlay(self.shortcut_window)
self.get_application().set_accels_for_action("win.show-help-overlay", ['<primary>slash'])
self.get_application().create_action('new_chat', lambda *_: self.new_chat(), ['<primary>n'])
@@ -1789,6 +1818,8 @@ Generate a title following these rules:
self.get_application().create_action('create_model_from_existing', lambda *_: dialogs.create_model_from_existing(self))
self.get_application().create_action('create_model_from_file', lambda *_: dialogs.create_model_from_file(self))
self.get_application().create_action('create_model_from_name', lambda *_: dialogs.create_model_from_name(self))
self.get_application().create_action('duplicate_chat', self.chat_actions)
self.get_application().create_action('duplicate_current_chat', self.current_chat_actions)
self.get_application().create_action('delete_chat', self.chat_actions)
self.get_application().create_action('delete_current_chat', self.current_chat_actions)
self.get_application().create_action('rename_chat', self.chat_actions)

View File

@@ -100,6 +100,9 @@
</style>
<property name="popover">
<object class="GtkPopover" id="model_popover">
<style>
<class name="model_popover"/>
</style>
<property name="has-arrow">false</property>
<child>
<object class="GtkBox">
@@ -992,6 +995,10 @@
<attribute name="label" translatable="yes">Rename Chat</attribute>
<attribute name="action">app.rename_current_chat</attribute>
</item>
<item>
<attribute name="label" translatable="yes">Duplicate Chat</attribute>
<attribute name="action">app.duplicate_current_chat</attribute>
</item>
<item>
<attribute name="label" translatable="yes">Export Chat</attribute>
<attribute name="action">app.export_current_chat</attribute>
@@ -1014,6 +1021,10 @@
<attribute name="label" translatable="yes">Rename Chat</attribute>
<attribute name="action">app.rename_chat</attribute>
</item>
<item>
<attribute name="label" translatable="yes">Duplicate Chat</attribute>
<attribute name="action">app.duplicate_chat</attribute>
</item>
<item>
<attribute name="label" translatable="yes">Export Chat</attribute>
<attribute name="action">app.export_chat</attribute>