feat(desktop): screenshot popup close button, remembered mini timer position, custom and start/stop sounds
Merged
EV
15d ago+3308+3308+0123456789012345678901234567890123456789-61-61-01234567890123456789
Summary
Four small UX changes to the desktop apps (Gauzy Desktop, Desktop Timer, Agent):
- #7643: the screenshot notification has a close (X) button that hides it at once.
- #7683: the mini timer (always-on widget) opens where it was last left, as long as that spot is still on a connected display.
- #6715: each computer can pick its own screenshot sound (WAV, MP3 or OGG, 10 MB at most) and go back to the default sound. The sound stays on that computer.
- #7141: optional start and stop sounds for the timer. They are off by default, so existing users notice nothing until they turn them on.
Nothing is removed. Every default keeps today's behaviour.
Decisions
- "Sound Enabled" is unchanged. It still covers the screenshot sound only. The start/stop sounds keep their own switch, and there is no master mute in this PR.
- The midnight rollover is silent. It stops the timer and starts it again for the new day. That is an internal split, not the user starting or stopping, so it plays no cue. If that restart fails and leaves the timer stopped, the stop cue plays.
- Mini timer positions are kept per size. The compact (60Ć110) and expanded (270Ć45) widgets differ in shape and the mode is a setting, so each keeps its own spot. A shared position would put the wide bar where the small widget fitted and clamp it.
- The timer sounds read only the sound settings. They use a small
GET_SOUND_SETTINGShandler. The existingapp_settinghandler is untouched.
Why
- #7643: until now the screenshot popup could only be waited out (it hides 3 s after the capture).
- #7683: the mini timer always opened at the left edge of the primary display, so users moved it again on every launch.
- #6715: the screenshot sound was fixed to the bundled file.
- #7141: there was no audible confirmation that tracking started or stopped.
What changed
@gauzy/desktop-window
ScreenCaptureNotification:- New
hideNow()hides the window immediately and drops the pending delayed hide. hide()keeps its 3 s delay but restarts it when called again, so a notification shown in the meantime stays up its full time.- Each notification window listens on
close_screen_capture_notification. It only accepts the message from its own page (event.sendercheck) and stops listening once it is destroyed. acceptFirstMouse: truelets the first click reach the never-focused window on macOS.
- New
- New
utils/window-position.ts(pure):restoreWindowPosition()puts a saved position back on the display it overlaps most, clamped so the window is whole on that display. It returnsnullwhen the spot is on no connected display (for example a monitor was unplugged). - New
utils/window-position-memory.ts:WindowPositionMemorysaves the position 500 ms after a drag rests, and again when the window is destroyed. A save that failed is tried again on close. It writes to the electron-store keywindowPositions, kept apart fromappSetting, which the settings page writes back whole. AlwaysOnrestores the position separately for the compact and expanded sizes. It falls back to today's default (x 16, vertically centred on the primary display) when nothing usable is saved.- Each widget now takes its size from
AlwaysOn.SIZESfor its own mode. The old mutable static size made a compact widget opened after an expanded one in the same process 270Ć45.
@gauzy/desktop-ui-lib
screen-capture: ghost close button (close-outline icon, translatedaria-label/title) at the right of the title row.- New
settings/screenshot-sound/:ScreenshotSoundSettingServiceasks the main process to choose or reset the sound, explains a refused file, and confirms a change. The file dialog texts come from the page, so they are in the user's language. settings.component:- Screen Capture menu, Sound Notifications section: the current screenshot sound, Choose sound... and Use default.
- Timer menu, new "Timer Sounds" heading: the switch "Play a short sound when the timer starts or stops".
- It does not depend on screenshot capture being allowed.
- It is hidden in the Agent, which has no timer.
- New
time-tracker/sounds/:timerSoundCues(quiet$): a pure rxjs operator that turns the time-tracker store's ignition changes intostart/stopcues. A run of this computer cues a start and its stop. Silent cases:- a failed start;
- a silent restart to change the task or project, including the
STARTEDwithout a mode that the task change dialog sets afterwards; - another device's timer that this computer only displays;
- whatever was already true when it started watching.
- Quiet stretches: while
quiet$is true, changes are followed but not cued. When the stretch ends, only its net effect is cued. timer-sound-tones.ts: two short synthesized notes per cue with the Web Audio API, so there is no audio file to ship or license. Start goes up (E5 to A5) and stop goes down (A5 to D5), with a fade in and out to avoid clicks. Apps can supply their own notes through theTIMER_SOUND_TONEStoken.TimerSoundService:- Reads only the sound settings (
DesktopSoundSettingsChannel.GET) on each cue, so switching the setting applies at once. quietly(work)runs an internal split without cues.- Opens one
AudioContextlazily, resumes it if suspended, and never throws.
- Reads only the sound settings (
SoundFilePlayer:- The
play_soundlistener now makes a new audio element when the file changes. A newly chosen sound is heard from the next screenshot onward without restarting, and an empty first message no longer throws. - File system paths become encoded file URLs (
soundFileUrl()), so#,?,%or a space in a folder name no longer cut the path.
- The
time-tracker.component.ts: only call sites. These are:- the field;
- one
watch(...)subscription; - the
play_soundlistener delegating toSoundFilePlayer; - the midnight rollover wrapped in
quietly().
@gauzy/desktop-lib
- New
screenshot-sound/:- Pure resolver: a stored path is only played while it is a supported file inside
userData/sounds. - Service:
- Imports the file: checks the extension, that it is a regular file and the size limit, then copies it to
userData/sounds/screenshot-sound-<uuid>.<ext>. - Reset goes back to the bundled sound.
- Deletes the previous copy, and the new copy when the setting could not be saved. It only ever deletes files in that folder.
- Imports the file: checks the extension, that it is a regular file and the size limit, then copies it to
- IPC handlers
SELECT_SCREENSHOT_SOUND/RESET_SCREENSHOT_SOUND.
- Pure resolver: a stored path is only played while it is a supported file inside
- New
sound-settings/:GET_SOUND_SETTINGSanswers with the sound settings only ({ timerStartStopSound }). desktop-ipc.tsregisters both modules' handlers inipcMainHandlerand removes them inremoveAllHandlers().desktop-screenshot.ts: both screenshot paths play throughplayScreenshotSound().
@gauzy/contracts
IDesktopScreenshotSound,IDesktopScreenshotSoundResult,DesktopScreenshotSoundChannel,DesktopScreenshotSoundError.IDesktopSoundSettings,DesktopSoundSettingsChannel.
@gauzy/desktop-core
appSetting.customScreenshotSoundandappSetting.timerStartStopSound.- The
windowPositionsstore key and theIWindowPositiontype.
Agent (apps/agent)
- Registers the screenshot-sound handlers next to its other IPC handlers.
ScreenCaptureWindow.hideNow()closes the notification through the window manager'shideNotificationWindow(). That cancels the manager's own auto-hide timeout, so it cannot close the next notification early.isWindowReadyToShow()stops waiting when the window was closed or replaced meanwhile, instead of polling a destroyed window forever.
i18n
- English texts in
packages/ui-core/i18n/assets/i18n/en.json(TIMER_TRACKER.SETTINGS.*). Other languages fall back to English until translated.
Flags / settings and defaults
| Setting | Default | Effect of the default |
|---|---|---|
appSetting.timerStartStopSound | false | No start/stop sounds, as today |
appSetting.customScreenshotSound | null | The bundled screenshot sound, as today |
electron-store windowPositions | absent | Mini timer at today's default position until the user moves it |
| Screenshot notification auto-hide | 3 s (ScreenCaptureNotification.AUTO_HIDE_DELAY_MS) | Unchanged |
Tests
Run from the repository root with the workspace Node (v26):
| Command | Result |
|---|---|
npx jest -c packages/desktop-window/jest.config.ts --rootDir packages/desktop-window | 6 suites, 67 tests passed |
NODE_OPTIONS=--experimental-vm-modules npx jest -c packages/desktop-lib/jest.config.ts --rootDir packages/desktop-lib src/lib/sound-settings src/lib/screenshot-sound src/lib/desktop-screenshot.spec.ts | 6 suites, 46 tests passed |
npx jest -c packages/desktop-ui-lib/jest.config.ts --rootDir packages/desktop-ui-lib src/lib/settings src/lib/screen-capture src/lib/time-tracker/sounds src/lib/time-tracker/time-tracker.component.spec.ts | 26 suites, 97 tests passed |
npx jest -c apps/agent/jest.config.ts --rootDir apps/agent src/main/window/screen-capture-window.spec.ts src/main/window-manager.spec.ts | 2 suites, 8 tests passed |
npx jest -c packages/desktop-core/jest.config.ts --rootDir packages/desktop-core | 1 suite, 2 tests passed |
npx tsc --noEmit -p packages/<contracts, desktop-core, desktop-lib, desktop-window>/tsconfig.lib.json and the desktop-lib / desktop-window tsconfig.spec.json | No errors |
npx tsc --noEmit -p packages/desktop-ui-lib/tsconfig.lib.json and tsconfig.spec.json | No errors in the changed files. The only lib errors are the existing Cannot find namespace 'google' ones in packages/ui-core/shared. |
The Agent's changed window files also type-check against the package sources. The default agent tsconfig.electron.json resolves @gauzy/* to another checkout's builds here.
eslint on the new and changed TypeScript files reports no new errors, only no-explicit-any warnings in specs, which matches existing spec style.
Manual verification still needed
These were not verifiable here: they need a running Electron build on Windows, macOS and Linux.
- #7643
- Take a screenshot and click the X: the popup hides at once.
- On macOS, check that the first click works on the non-focused popup.
- Take two screenshots within 3 s: the second popup stays its full 3 s.
- Agent: click X, then take another screenshot at once: the new popup stays its full time.
- #7683
- Move the mini timer, quit and relaunch: it opens where it was.
- Expand it, move it, relaunch: the expanded position is remembered separately.
- Unplug the monitor it was on: it opens at the default position.
- #6715
- Choose an MP3, WAV or OGG and take a screenshot: the new sound plays without a restart. Check also with a
#or a space in the user folder name. - Use default brings the bundled sound back.
- A file over 10 MB or of another type is refused with a message.
- With the sound muted, nothing plays, including in the in-app screenshot preview.
- The Agent's settings show the chooser and its screenshots use the sound.
- Choose an MP3, WAV or OGG and take a screenshot: the new sound plays without a restart. Check also with a
- #7141
- The switch is in the Timer menu and off on a fresh profile.
- Turn it on: start gives a rising cue and stop a falling one.
- Changing the task while tracking is silent, and the later stop still plays.
- Following another device's timer is silent.
- The midnight rollover is silent.
- Turning the switch off applies at once.
Risk and rollback
- Low risk. The start/stop sounds and the custom sound are opt-in. The window position is only used when it is on a connected display.
- Behaviour change, a bug fix: the in-app screenshot preview (
showCapturedToRenderer) used to play the screenshot sound even with the sound muted. It now honours the mute, like the notification path already did. - With the start/stop sounds on, a run of this computer that is stopped from elsewhere (another device, quit or logout, an error) plays the stop cue, because this computer did stop tracking.
- Rollback: revert the PR. The new store values (
customScreenshotSound,timerStartStopSound,windowPositions) are ignored by older builds. A copied sound file stays inuserData/soundsuntil removed.
Closes #7643
Closes #7683
Closes #6715
Closes #7141
š¤ Generated with Claude Code