Skip to content

feat: add auto-save speech composition drafts and crash recovery - #1193

Merged
itzzavdhesh merged 2 commits into
itzzavdhesh:mainfrom
hrshjswniii:feature/Auto-Save-Speech-Drafts
Aug 6, 2026
Merged

feat: add auto-save speech composition drafts and crash recovery#1193
itzzavdhesh merged 2 commits into
itzzavdhesh:mainfrom
hrshjswniii:feature/Auto-Save-Speech-Drafts

Conversation

@hrshjswniii

@hrshjswniii hrshjswniii commented Jul 30, 2026

Copy link
Copy Markdown
Contributor

🚀 Program

GSSoC

📝 Description

This PR implements Auto-Save Speech Composition Drafts & Crash Recovery in VoiceForge.jsx and TextToSpeech.jsx, preserving typed text compositions in real-time across accidental browser refreshes, tab closures, or call dropouts.

Key additions:

  1. In-Call Draft Auto-Save (TextToSpeech.jsx): Initialized in-call text state from localStorage.getItem("voiceforge:draft_call_speech") and auto-saved input changes on type.
  2. Main Composer Crash Recovery (VoiceForge.jsx): Initialized composer inputText state from localStorage.getItem("voiceforge:draft_speech") and persisted updates automatically.
  3. Clean Storage Clear on Submit: Automatically removed draft keys upon successful speech generation or explicit input clearing.
  4. Unit Test Suite (TextToSpeech.test.jsx): Added Vitest unit test assertions verifying component rendering and draft persistence handling.

🔗 Related Issue

Closes #1187

🔄 Type of Change

  • 🐛 Bug fix
  • ✨ New feature
  • 🔍 SEO improvement
  • 🎨 Style / UI improvement
  • ♿ Accessibility improvement
  • 📝 Documentation
  • ⚙️ CI / configuration
  • 🧹 Refactor / cleanup

🧪 How to Test

  1. Open http://localhost:5173 in a browser.
  2. Go to the Call page or composer view and type a message without clicking Speak.
  3. Refresh the browser tab or navigate to Settings and back to Call.
  4. Verify the typed draft text is automatically restored in the text box.
  5. Click Speak and verify the draft key is cleared cleanly.
  6. Run client tests: npm run test --workspace client and verify all tests pass.

✅ Checklist

  • I am contributing under GSSoC
  • My code follows the project's existing style
  • I have tested my changes in a browser
  • I have linked the related issue above
  • My PR title follows Conventional Commits format (e.g. feat: add auto-save speech composition drafts and crash recovery)

Summary by cubic

Adds auto-save for speech drafts in the composer and in-call views to prevent text loss on refresh, tab close, or crashes. Drafts are restored on load and cleared after speaking or when the input is emptied.

  • New Features
    • Persist drafts to localStorage with voiceforge:draft_speech and voiceforge:draft_call_speech, using safe reads/writes with graceful fallback.
    • Increased composer character limit to 500.
    • Added tests for component rendering and draft persistence behavior.

Written for commit 9ffd76a. Summary will update on new commits.

Review in cubic

Summary by CodeRabbit

  • New Features

    • Draft text is now automatically saved and restored between sessions.
    • Saved drafts are cleared when submitted, cleared manually, or emptied.
    • The voice composer now supports up to 500 characters.
  • Bug Fixes

    • Improved resilience when local storage is unavailable.
  • Tests

    • Added coverage for text rendering and draft persistence behavior.

@vercel

vercel Bot commented Jul 30, 2026

Copy link
Copy Markdown

@hrshjswniii is attempting to deploy a commit to the itzzavdhesh's projects Team on Vercel.

A member of the Team first needs to authorize it.

@github-actions

github-actions Bot commented Jul 30, 2026

Copy link
Copy Markdown

✍️ DCO Sign-off Needed

Hey @hrshjswniii! 👋 One or more commits in this PR are missing a Signed-off-by: line.

Warning

  • e8d7cd3 feat: add auto-save speech composition drafts and crash recovery
  • 9ffd76a Merge branch 'main' into feature/Auto-Save-Speech-Drafts

How to fix:

For the latest commit:

git commit --amend --signoff
git push --force-with-lease

For multiple commits, replace N with the number to update:

git rebase --signoff HEAD~N
git push --force-with-lease

This comment will update automatically after you push.


🤖 VoiceForge Automation · Updates automatically on edits

@coderabbitai

coderabbitai Bot commented Jul 30, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

TextToSpeech and VoiceForge now persist speech drafts in localStorage, restore saved text, and handle storage failures. TextToSpeech clears saved drafts after submission. VoiceForge increases the character limit to 500. Tests cover draft persistence and fallback behavior.

Changes

Draft persistence and composer behavior

Layer / File(s) Summary
Draft storage integration
client/src/components/TextToSpeech.jsx, client/src/components/VoiceForge.jsx, client/src/components/TextToSpeech.test.jsx
Both composers save and restore voiceforge:draft_speech. Storage failures are ignored. VoiceForge raises the character limit to 500. Tests cover rendering, persistence, removal, and unavailable storage.
Submission draft cleanup
client/src/components/TextToSpeech.jsx
TextToSpeech removes the saved draft after successful submission.

Estimated code review effort: 3 (Moderate) | ~20 minutes

Sequence Diagram(s)

sequenceDiagram
  participant User
  participant TextToSpeech
  participant VoiceForge
  participant localStorage
  User->>TextToSpeech: Enter speech text
  TextToSpeech->>localStorage: Save voiceforge:draft_speech
  User->>VoiceForge: Open composer
  VoiceForge->>localStorage: Read voiceforge:draft_speech
  localStorage-->>VoiceForge: Return saved draft
  User->>TextToSpeech: Submit speech
  TextToSpeech->>localStorage: Remove voiceforge:draft_speech
Loading

Possibly related PRs

Suggested labels: type:feature, pr-validated, template-ok, linked-issue-ok, assignee-match, quality:clean

Suggested reviewers: vivek0028, itzzavdhesh, 4f4d

🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (2 warnings)

Check name Status Explanation Resolution
Linked Issues check ⚠️ Warning The PR implements draft save, restore, and clear behavior, but retained activeEmotion references can break TextToSpeech. Restore the activeEmotion state declaration or remove all remaining references, then verify TextToSpeech draft behavior.
Out of Scope Changes check ⚠️ Warning The 300-to-500 character limit change in VoiceForge is unrelated to Issue #1187 and the stated draft persistence objectives. Move the character-limit change to a separate PR or provide a linked requirement that justifies changing the limit.
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes automatic draft saving and crash recovery, which are the primary changes described in the PR.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
✨ Finishing Touches 💡 1
🛠️ Fix failing CI checks 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@github-actions

github-actions Bot commented Jul 30, 2026

Copy link
Copy Markdown

🎉 PR Ready for Mentor Review

Hey @hrshjswniii! 👋 Your PR passed all checks and is now in the GSSoC review queue.

Note

🔗 Closing: #1187 · 📐 83 lines across 3 file(s) · 📬 Review requested

@sabeenaviklar @Anushreebasics @itsdakshjain @snehkris @Mrigakshi-Rathore @Itzzavdheshh @Nitya-003 @4f4d @lovestaco, this PR is ready for your review — please confirm scope, check behavior and tests, then approve or request changes.

Important

This is not an approval. Please wait for mentor feedback before expecting a merge. If changes are requested, push them to this same branch and keep the PR focused on the linked issue.


🤖 VoiceForge Automation · Updates automatically on edits

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

3 issues found across 3 files

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="client/src/components/TextToSpeech.test.jsx">

<violation number="1" location="client/src/components/TextToSpeech.test.jsx:9">
P3: `toBeDefined()` after `getByText()`/`getByPlaceholderText()` is redundant — these queries throw if the element is absent, so the assertion never fails. Use `toBeInTheDocument()` (from @testing-library/jest-dom) for a meaningful presence check, or drop the matcher and rely on the query throwing.</violation>

<violation number="2" location="client/src/components/TextToSpeech.test.jsx:15">
P1: The "handles draft localStorage persistence safely" test doesn't test the TextToSpeech component at all. It only exercises localStorage primitives directly, which is a browser/DOM implementation detail with no relation to the component's draft-read-on-mount or persist-on-typing behavior. Replace the test body with one that (1) sets localStorage before rendering, (2) renders TextToSpeech and asserts the textarea pre-fills from the draft, (3) simulates a typing event, and (4) asserts localStorage is updated. Drop the `expect(true).toBe(true)` tautology in the else branch — jsdom provides localStorage, and the fallback silently hides failures.</violation>
</file>

<file name="client/src/components/VoiceForge.jsx">

<violation number="1" location="client/src/components/VoiceForge.jsx:36">
P2: The draft key `voiceforge:draft_speech` persists in localStorage after "Speak & Save" because `handleSpeak` never clears `inputText`. The new `useEffect` persists `inputText` to localStorage on every change, and since `inputText` is unchanged after speaking, the old draft stays in localStorage. Next time the user visits VoiceForge, the text they already spoke will be restored. This differs from `TextToSpeech.jsx` where `submit()` explicitly calls `setText("")` which triggers the cleanup effect and removes the draft key.</violation>
</file>

Reply with feedback, questions, or to request a fix.

Re-trigger cubic


it("handles draft localStorage persistence safely", () => {
if (typeof localStorage !== "undefined") {
localStorage.setItem("voiceforge:draft_call_speech", "Draft speech text");

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1: The "handles draft localStorage persistence safely" test doesn't test the TextToSpeech component at all. It only exercises localStorage primitives directly, which is a browser/DOM implementation detail with no relation to the component's draft-read-on-mount or persist-on-typing behavior. Replace the test body with one that (1) sets localStorage before rendering, (2) renders TextToSpeech and asserts the textarea pre-fills from the draft, (3) simulates a typing event, and (4) asserts localStorage is updated. Drop the expect(true).toBe(true) tautology in the else branch — jsdom provides localStorage, and the fallback silently hides failures.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At client/src/components/TextToSpeech.test.jsx, line 15:

<comment>The "handles draft localStorage persistence safely" test doesn't test the TextToSpeech component at all. It only exercises localStorage primitives directly, which is a browser/DOM implementation detail with no relation to the component's draft-read-on-mount or persist-on-typing behavior. Replace the test body with one that (1) sets localStorage before rendering, (2) renders TextToSpeech and asserts the textarea pre-fills from the draft, (3) simulates a typing event, and (4) asserts localStorage is updated. Drop the `expect(true).toBe(true)` tautology in the else branch — jsdom provides localStorage, and the fallback silently hides failures.</comment>

<file context>
@@ -0,0 +1,22 @@
+
+  it("handles draft localStorage persistence safely", () => {
+    if (typeof localStorage !== "undefined") {
+      localStorage.setItem("voiceforge:draft_call_speech", "Draft speech text");
+      expect(localStorage.getItem("voiceforge:draft_call_speech")).toBe("Draft speech text");
+      localStorage.removeItem("voiceforge:draft_call_speech");
</file context>

try {
if (typeof localStorage !== "undefined") {
if (inputText) {
localStorage.setItem(COMPOSE_DRAFT_KEY, inputText);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: The draft key voiceforge:draft_speech persists in localStorage after "Speak & Save" because handleSpeak never clears inputText. The new useEffect persists inputText to localStorage on every change, and since inputText is unchanged after speaking, the old draft stays in localStorage. Next time the user visits VoiceForge, the text they already spoke will be restored. This differs from TextToSpeech.jsx where submit() explicitly calls setText("") which triggers the cleanup effect and removes the draft key.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At client/src/components/VoiceForge.jsx, line 36:

<comment>The draft key `voiceforge:draft_speech` persists in localStorage after "Speak & Save" because `handleSpeak` never clears `inputText`. The new `useEffect` persists `inputText` to localStorage on every change, and since `inputText` is unchanged after speaking, the old draft stays in localStorage. Next time the user visits VoiceForge, the text they already spoke will be restored. This differs from `TextToSpeech.jsx` where `submit()` explicitly calls `setText("")` which triggers the cleanup effect and removes the draft key.</comment>

<file context>
@@ -15,9 +15,34 @@ import { LanguageSelector } from "./LanguageSelector.jsx";
+    try {
+      if (typeof localStorage !== "undefined") {
+        if (inputText) {
+          localStorage.setItem(COMPOSE_DRAFT_KEY, inputText);
+        } else {
+          localStorage.removeItem(COMPOSE_DRAFT_KEY);
</file context>

describe("TextToSpeech component and draft persistence", () => {
it("renders TextToSpeech component and character limit indicator", () => {
render(<TextToSpeech onSpeak={() => {}} />);
expect(screen.getByText("Type to speak")).toBeDefined();

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P3: toBeDefined() after getByText()/getByPlaceholderText() is redundant — these queries throw if the element is absent, so the assertion never fails. Use toBeInTheDocument() (from @testing-library/jest-dom) for a meaningful presence check, or drop the matcher and rely on the query throwing.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At client/src/components/TextToSpeech.test.jsx, line 9:

<comment>`toBeDefined()` after `getByText()`/`getByPlaceholderText()` is redundant — these queries throw if the element is absent, so the assertion never fails. Use `toBeInTheDocument()` (from @testing-library/jest-dom) for a meaningful presence check, or drop the matcher and rely on the query throwing.</comment>

<file context>
@@ -0,0 +1,22 @@
+describe("TextToSpeech component and draft persistence", () => {
+  it("renders TextToSpeech component and character limit indicator", () => {
+    render(<TextToSpeech onSpeak={() => {}} />);
+    expect(screen.getByText("Type to speak")).toBeDefined();
+    expect(screen.getByPlaceholderText("Type what you want to say...")).toBeDefined();
+  });
</file context>

@Nitya-003 Nitya-003 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@hrshjswniii Resolve the conflicts and comments.

@itzzavdhesh
itzzavdhesh removed the request for review from 1754riya August 4, 2026 02:53

@itsdakshjain itsdakshjain left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This one also has same verdict
Conflicts presents with unresolved comments

@itsdakshjain itsdakshjain added the mentor:itsdakshjain GSSoC: Mentor-@itsdakshjain label Aug 4, 2026
@github-actions github-actions Bot added mentor:Nitya-003 Applied by mentor automation needs revision and removed ready for review labels Aug 6, 2026
@github-actions

github-actions Bot commented Aug 6, 2026

Copy link
Copy Markdown

🔄 Changes Requested

Hey @hrshjswniii! 👋 A mentor has reviewed your PR and requested some changes.

Warning

Please review the feedback above, update this same branch, and keep the PR focused on the linked issue.

Once you push your updates, the review flow will continue automatically on this same PR.


🤖 VoiceForge Automation · Updates automatically on edits

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🧹 Nitpick comments (1)
client/src/components/TextToSpeech.test.jsx (1)

13-21: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Test TextToSpeech draft behavior through the component.

This test only verifies the browser localStorage API. It does not test draft restoration, input persistence, explicit clearing, or cleanup after a successful onSpeak call.

Preload the draft key before rendering. Assert the textarea value. Change and clear the textarea. Then assert the stored value is added and removed by TextToSpeech.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@client/src/components/TextToSpeech.test.jsx` around lines 13 - 21, Replace
the API-only test in the draft persistence case with a rendered TextToSpeech
interaction test: preload the voiceforge:draft_call_speech key before rendering,
assert the textarea restores that value, then change and clear the textarea
while verifying the component adds and removes the stored draft. Also invoke the
successful onSpeak flow and verify the draft is cleaned up afterward, using the
component’s existing test selectors and handlers.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@client/src/components/TextToSpeech.jsx`:
- Around line 69-79: Restore the activeEmotion state declaration inside
TextToSpeech alongside the existing text state, exposing both activeEmotion and
setActiveEmotion for the reads and updates already used in the component.
Initialize it to the component’s expected default emotion value.

In `@client/src/components/VoiceForge.jsx`:
- Around line 35-47: Update handleSpeak to inspect the explicit success result
returned by speak and call setInputText("") only when speech generation
succeeds; preserve the current input and draft when speak fails. Ensure speak
returns a success indicator on successful completion and a failure result on
errors.

---

Nitpick comments:
In `@client/src/components/TextToSpeech.test.jsx`:
- Around line 13-21: Replace the API-only test in the draft persistence case
with a rendered TextToSpeech interaction test: preload the
voiceforge:draft_call_speech key before rendering, assert the textarea restores
that value, then change and clear the textarea while verifying the component
adds and removes the stored draft. Also invoke the successful onSpeak flow and
verify the draft is cleaned up afterward, using the component’s existing test
selectors and handlers.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: c107032f-b48f-49db-9359-788b5bb78280

📥 Commits

Reviewing files that changed from the base of the PR and between 63b06a8 and 9ffd76a.

📒 Files selected for processing (3)
  • client/src/components/TextToSpeech.jsx
  • client/src/components/TextToSpeech.test.jsx
  • client/src/components/VoiceForge.jsx

Comment on lines 69 to +79
export default function TextToSpeech({ onSpeak, disabled = false, status = "idle" }) {
const [text, setText] = React.useState("");
const [activeEmotion, setActiveEmotion] = React.useState("neutral");
const [text, setText] = React.useState(() => {
try {
if (typeof localStorage !== "undefined") {
return localStorage.getItem(DRAFT_KEY) || "";
}
} catch {
// Storage unavailable
}
return "";
});

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🔴 Critical | ⚡ Quick win

Restore the activeEmotion state declaration.

activeEmotion is read at Line 118. setActiveEmotion is called at Line 206. Neither identifier is declared after this change. Rendering TextToSpeech throws a ReferenceError.

Proposed fix
 export default function TextToSpeech({ onSpeak, disabled = false, status = "idle" }) {
+  const [activeEmotion, setActiveEmotion] = React.useState("neutral");
   const [text, setText] = React.useState(() => {
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
export default function TextToSpeech({ onSpeak, disabled = false, status = "idle" }) {
const [text, setText] = React.useState("");
const [activeEmotion, setActiveEmotion] = React.useState("neutral");
const [text, setText] = React.useState(() => {
try {
if (typeof localStorage !== "undefined") {
return localStorage.getItem(DRAFT_KEY) || "";
}
} catch {
// Storage unavailable
}
return "";
});
export default function TextToSpeech({ onSpeak, disabled = false, status = "idle" }) {
const [activeEmotion, setActiveEmotion] = React.useState("neutral");
const [text, setText] = React.useState(() => {
try {
if (typeof localStorage !== "undefined") {
return localStorage.getItem(DRAFT_KEY) || "";
}
} catch {
// Storage unavailable
}
return "";
});
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@client/src/components/TextToSpeech.jsx` around lines 69 - 79, Restore the
activeEmotion state declaration inside TextToSpeech alongside the existing text
state, exposing both activeEmotion and setActiveEmotion for the reads and
updates already used in the component. Initialize it to the component’s expected
default emotion value.

Comment on lines +35 to +47
useEffect(() => {
try {
if (typeof localStorage !== "undefined") {
if (inputText) {
localStorage.setItem(COMPOSE_DRAFT_KEY, inputText);
} else {
localStorage.removeItem(COMPOSE_DRAFT_KEY);
}
}
} catch {
// Storage unavailable
}
}, [inputText]);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Clear the compose draft after successful speech submission.

handleSpeak does not clear inputText after speak(text) succeeds. This effect therefore keeps voiceforge:draft_speech after submission. A later refresh restores an already sent message.

Return an explicit success result from speak. When that result is successful, call setInputText(""). Do not clear the draft when speech generation fails.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@client/src/components/VoiceForge.jsx` around lines 35 - 47, Update
handleSpeak to inspect the explicit success result returned by speak and call
setInputText("") only when speech generation succeeds; preserve the current
input and draft when speak fails. Ensure speak returns a success indicator on
successful completion and a failure result on errors.

@itzzavdhesh itzzavdhesh added gssoc:approved gssoc:approved type:accessibility type type:bug type:bug type:performance type:performance level:intermediate level:intermediate quality:clean quality:clean labels Aug 6, 2026
@itzzavdhesh
itzzavdhesh merged commit 81dc0f1 into itzzavdhesh:main Aug 6, 2026
8 of 15 checks passed
@github-actions

github-actions Bot commented Aug 6, 2026

Copy link
Copy Markdown

🎊 PR Merged Successfully

Hey @hrshjswniii! 👋 Congratulations and thank you for your contribution to VoiceForge!

Note

🔗 Linked issue(s): #1187 · ✅ Marked as merged and complete

Maintainers may still handle final cleanup, release notes, or follow-up tracking after the merge.


🤖 VoiceForge Automation · Updates automatically on edits

@itzzavdhesh itzzavdhesh added SSoC26 SSOC Hard hard ELUSOC ELUSOC VETERAN VETERAN type:feature type:feature type:refactor type:refactor level:advanced level:advanced mentor:Anushreebasics GSSoC: Mentor-@Anushreebasics mentor:sabeenaviklar GSSoC: Mentor-@sabeenaviklar and removed level:intermediate level:intermediate labels Aug 12, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

feat: Auto-Save Speech Composition Drafts & Crash Recovery

4 participants