momolulu 1.0.17 → 1.1.0

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/README.en.md CHANGED
@@ -1,120 +1,55 @@
1
1
  # momolulu
2
2
 
3
- A gentle companion for DeepSeek Harness (DSH): scenic photos, a focus timer, wellbeing reminders, weather, and a cross-session radar.
3
+ A companion photo for DeepSeek Harness: focus timing, water reminders, weather, and a radar showing which session just finished.
4
4
 
5
5
  [中文](README.md) | **English**
6
6
 
7
- [![npm version](https://img.shields.io/npm/v/momolulu.svg)](https://www.npmjs.com/package/momolulu)
8
- [![npm downloads](https://img.shields.io/npm/dm/momolulu.svg)](https://www.npmjs.com/package/momolulu)
7
+ [![npm](https://img.shields.io/npm/v/momolulu.svg)](https://www.npmjs.com/package/momolulu)
8
+ [![downloads](https://img.shields.io/npm/dm/momolulu.svg)](https://www.npmjs.com/package/momolulu)
9
9
  [![license](https://img.shields.io/npm/l/momolulu.svg)](LICENSE)
10
- [![Publish](https://github.com/YinniCoding/momolulu/actions/workflows/publish.yml/badge.svg)](https://github.com/YinniCoding/momolulu/actions/workflows/publish.yml)
11
10
 
12
11
  ```sh
13
12
  dsh plugin --profile web add momolulu
14
13
  ```
15
14
 
16
- ![momolulu overview: the DSH chat surface with the companion photo in the lower right](docs/overview.webp)
15
+ ![momolulu overview](docs/overview.webp)
17
16
 
18
- ## Features
17
+ ## What it does
19
18
 
20
- - **Photo companion**: a floating photo card with subtle drift and breathing motion; collapse it to an avatar with a progress ring and countdown.
21
- - **Focus timer**: 15 / 25 / 45 / 60 minutes, with pause, resume and reset. Starting focus requests fullscreen; Esc exits fullscreen without interrupting the timer.
22
- - **Break reminders**: every 20 minutes by default, a gentle nudge for water and a stretch. The floating widget shakes briefly and the reminder row highlights; choose "Got it" or "Remind in 10 min". An 8-second preview is available.
23
- - **Session radar**: lists ordinary conversations that are running, ended-but-unread, waiting for input, or requesting permission. Click a row to open it. It never switches conversations automatically, answers questions, or grants permissions.
24
- - **Weather**: Nanjing by default, via Open-Meteo. Location is requested only after you click "Use my location".
25
- - **Reading protection**: reduces background distraction over the chat area while keeping the scenery on the right. Background brightness and opacity are adjustable.
26
- - Chinese and English UI copy, with support for the system's reduced-motion setting.
19
+ - **Photo companion**: a scenic card in the lower right with gentle motion; collapse it to an avatar with a countdown.
20
+ - **Focus timer**: 15/25/45/60 minutes, pausable. Optionally fullscreen; Esc leaves fullscreen without stopping the timer.
21
+ - **Break reminders**: every 20 minutes, a brief nudge to drink water and stretch, with a shake and highlight; optional chime (off by default).
22
+ - **Session radar**: running, just-finished, waiting-for-input and permission-requesting sessions; click one to jump to it.
23
+ - **Weather**: Open-Meteo, Nanjing by default; location is requested only after you click "Use my location".
27
24
 
28
- ## Install
25
+ Typing `/focus` in the composer also starts or pauses the timer.
29
26
 
30
- Built for **DSH 0.2.1-rc.5 through 0.2.x**; verified mainly on 0.2.2-rc.1, earlier 0.2.x releases are untested.
27
+ Focus requests fullscreen as an optional touch; a denied request never affects the timer.
31
28
 
32
- Published on npm. Install it in one line (replace `web` with your profile name):
29
+ ## Install
33
30
 
34
31
  ```sh
35
32
  dsh plugin --profile web add momolulu
36
33
  ```
37
34
 
38
- Desktop builds can also use **Plugins → Add plugin** and enter `momolulu`.
39
-
40
- To pin a version, use the commit-pinned Git specification from the GitHub release notes; you can also clone this repository and hand its absolute directory to DSH's plugin manager. **No separate web server is needed.**
41
-
42
- > **Already using the older local package?** Disable or uninstall `@local/lumululu-mei` before enabling `momolulu`; both render the same companion. The browser storage key `llm.companion.v2` is unchanged, so preferences and timer data continue on the same page origin. Back up first if you may need to roll back.
35
+ Desktop: **Plugins → Add plugin → `momolulu`**, then restart once.
43
36
 
44
- ### Install from npm
45
-
46
- ```sh
47
- npm install -g momolulu
48
- ```
49
-
50
- Useful if you manage global packages yourself; inside DSH, prefer `dsh plugin add` above.
51
-
52
- ### Add the Discover index
53
-
54
- If your DSH profile has the optional plugin-market Discover feature enabled, add this self-hosted index source:
55
-
56
- ```
57
- https://raw.githubusercontent.com/YinniCoding/momolulu/main/market.json
58
- ```
59
-
60
- This is a **maintainer-hosted index**, not a claim of inclusion in a central or official marketplace. The index pins a release commit; the package is not published to npm.
61
-
62
- ## Use
63
-
64
- - Click the photo to open the focus panel. The control beside the countdown starts or pauses without opening the panel.
65
- - Click `−` to minimize to an avatar; click the avatar to bring the photo back.
66
- - Open **Reminder settings → Test reminder (8 sec)** to preview the shake and highlight without changing real reminder times.
67
- - Click the radar summary or the avatar badge to inspect other conversations; click a row to open it.
68
- - Open **Appearance** to switch scene and motion, toggle reading protection, and adjust background brightness and opacity.
69
- - Pausing, finishing a session, or unloading the plugin never forces fullscreen to exit; only Esc or "Exit fullscreen" does.
37
+ > Disable the older `@local/lumululu-mei` first; both would render the same companion.
70
38
 
71
39
  ## Privacy
72
40
 
73
- See [PRIVACY.md](PRIVACY.md). The plugin has no developer backend and no plugin analytics. Weather makes third-party requests; preferences and timer data stay in browser storage; location is optional.
74
-
75
- ## FAQ
76
-
77
- **Why do momolulu and lumululu-mei conflict?**
78
- They are two versions of the same plugin and would render the same companion twice. Disable or uninstall `@local/lumululu-mei` before enabling `momolulu`.
79
-
80
- **Why is the weather city Nanjing by default?**
81
- The default coordinates are Nanjing (32.06, 118.80). Location is requested only after you click "Use my location", and a page refresh returns to Nanjing.
82
-
83
- **Why does starting focus request fullscreen?**
84
- It is an optional bit of ceremony. A denied fullscreen request does not affect the timer, and Esc exits fullscreen without stopping it.
85
-
86
- **Which DSH versions are supported?**
87
- Declared support: `>=0.2.1-rc.5 <0.3.0`; verified mainly on `0.2.2-rc.1`.
88
-
89
- **Where is my data stored?**
90
- Preferences and timer state live in browser `localStorage` under `llm.companion.v2` and are never uploaded. The session radar only reads local DSH state.
91
-
92
- **How do I uninstall?**
93
- Remove `momolulu` through the DSH plugin manager. To also clear preferences, delete that localStorage key manually.
41
+ Preferences and timer state stay in browser `localStorage` and are never uploaded. The radar reads local session state only; the sole network request is weather. See [PRIVACY.md](PRIVACY.md).
94
42
 
95
43
  ## Known limitations
96
44
 
97
- - The radar lists ordinary conversations only, not child-agent conversations. "Ended" does not imply successful execution, and the unread state is page-local and may be lost on refresh.
98
- - Focus timer state is not shared between multiple windows.
99
- - The browser or Electron may deny fullscreen or location. Such failures do not prevent focus timing.
100
- - Timers and reminders are not OS alarms: closing DSH or suspending the device prevents timely delivery, and the countdown catches up when ticks resume.
101
- - The default weather city is Nanjing. Located coordinates are rounded to two decimal places and held only in memory.
102
- - Automated tests cover clock logic, radar classification and reminder decisions, and fullscreen control (with an injected test port). Cross-platform visual layout, real fullscreen, session navigation and permission events still require manual verification.
103
- - There is no built-in updater. Upgrade by uninstalling and reinstalling through the DSH plugin manager's supported flow.
45
+ Ordinary sessions only (no child agents); no cross-window timer sync; timers are not OS alarms and stop when DSH is closed; no built-in updater.
104
46
 
105
47
  ## Development
106
48
 
107
- Local checks need Node.js 20 or newer; there is no build step and no install script.
108
-
109
49
  ```sh
110
- npm run check
111
- npm test
112
- npm pack --dry-run
50
+ npm run check && npm test
113
51
  ```
114
52
 
115
53
  ## License
116
54
 
117
- - **Source code**: [MIT](LICENSE).
118
- - **Artwork** (the three scenic photos, icon and screenshot): [CC BY 4.0](LICENSE-ARTWORK.md); attribute as "momolulu artwork © 2026 YinniCoding, CC BY 4.0".
119
-
120
- Weather data: [Open-Meteo](https://open-meteo.com/) (CC BY 4.0). No affiliation with DeepSeek or the character-rights owners is implied; see [NOTICE.md](NOTICE.md).
55
+ Code [MIT](LICENSE); artwork [CC BY 4.0](LICENSE-ARTWORK.md). Weather data by [Open-Meteo](https://open-meteo.com/). Not affiliated with DeepSeek.
package/README.md CHANGED
@@ -1,120 +1,55 @@
1
1
  # momolulu
2
2
 
3
- 给 DeepSeek Harness(DSH)的一点温柔陪伴:照片背景、专注计时、休息提醒、天气,以及跨会话的「会话雷达」。
3
+ 给 DeepSeek Harness 加一张会陪你的照片:专注计时、喝水提醒、天气,还有能看到哪个会话跑完的会话雷达。
4
4
 
5
5
  **中文** | [English](README.en.md)
6
6
 
7
- [![npm version](https://img.shields.io/npm/v/momolulu.svg)](https://www.npmjs.com/package/momolulu)
8
- [![npm downloads](https://img.shields.io/npm/dm/momolulu.svg)](https://www.npmjs.com/package/momolulu)
7
+ [![npm](https://img.shields.io/npm/v/momolulu.svg)](https://www.npmjs.com/package/momolulu)
8
+ [![downloads](https://img.shields.io/npm/dm/momolulu.svg)](https://www.npmjs.com/package/momolulu)
9
9
  [![license](https://img.shields.io/npm/l/momolulu.svg)](LICENSE)
10
- [![Publish](https://github.com/YinniCoding/momolulu/actions/workflows/publish.yml/badge.svg)](https://github.com/YinniCoding/momolulu/actions/workflows/publish.yml)
11
10
 
12
11
  ```sh
13
12
  dsh plugin --profile web add momolulu
14
13
  ```
15
14
 
16
- ![momolulu 概览:DSH 聊天界面与右下角照片陪伴](docs/overview.webp)
15
+ ![momolulu 概览](docs/overview.webp)
17
16
 
18
- ## 功能
17
+ ## 它做什么
19
18
 
20
- - **照片陪伴**:右下角照片卡片,轻微浮动与缓慢呼吸动效;可收成带进度环和倒计时的小头像。
21
- - **专注计时**:15 / 25 / 45 / 60 分钟可选,暂停、继续、重置;开始专注时请求全屏,按 Esc 只退出全屏、不打断计时。
22
- - **休息提醒**:默认每 20 分钟提示喝水 / 站立,到点轻晃并高亮提醒行,可「收到」或「10 分钟后提醒」;提供 8 秒测试预览。
23
- - **会话雷达**:列出运行中、刚结束待查看、等待输入、请求权限的普通会话,点击整行直接跳转;不会自动切换、不会代答或授予权限。
24
- - **天气**:默认南京,经 Open-Meteo 查询;只有你主动点击「使用当前位置」时才请求定位。
25
- - **阅读保护**:默认减弱聊天区域的背景干扰,右侧保留景色;背景亮度与浓度可调。
26
- - 中英文界面文案,支持系统「减少动态效果」。
19
+ - **照片陪伴**:右下角一张风景照,轻微浮动;可缩成带倒计时的小头像。
20
+ - **专注计时**:15/25/45/60 分钟,可暂停。开始时可全屏,Esc 退出全屏但不停表。
21
+ - **休息提醒**:每 20 分钟提示喝水、起来活动,到点轻晃一下并高亮;可选提示音(默认关)。
22
+ - **会话雷达**:列出运行中、刚结束、等待输入、请求权限的会话,点一下就跳过去。
23
+ - **天气**:Open-Meteo,默认南京;点「使用当前位置」才会请求定位。
27
24
 
28
- ## 安装
25
+ 在输入框输入 `/focus` 也能开始或暂停计时。
29
26
 
30
- 面向 **DSH 0.2.1-rc.5 至 0.2.x**;主要在 0.2.2-rc.1 上验证,更早的 0.2.x 未逐一测试。
27
+ 开始专注会请求全屏,这是可选的氛围;被拒绝也不影响计时。
31
28
 
32
- 已发布到 npm,推荐一行安装(把 `web` 换成你的 profile 名):
29
+ ## 安装
33
30
 
34
31
  ```sh
35
32
  dsh plugin --profile web add momolulu
36
33
  ```
37
34
 
38
- 桌面版也可以在 **插件 → 添加插件** 里直接填 `momolulu`。
39
-
40
- 需要锁定版本时,可用 GitHub Release 说明里固定到 commit 的 Git 地址;也可以克隆本仓库,把目录绝对路径交给 DSH 插件管理器。**不需要另外启动 Web 服务。**
41
-
42
- > **已经在用旧本地包的用户**:启用 `momolulu` 前,请先停用或卸载 `@local/lumululu-mei`,两者会同时渲染同一个陪伴组件。浏览器本地存储键 `llm.companion.v2` 保持不变,同一页面源下偏好与计时数据可以延续;需要回滚时请先备份。
35
+ 桌面版:**插件 → 添加插件 → 输入 `momolulu`**,装完重启一次。
43
36
 
44
- ### 从 npm 安装
45
-
46
- ```sh
47
- npm install -g momolulu
48
- ```
49
-
50
- 这适合自己管理全局包的场景;DSH 里更推荐上面的 `dsh plugin add`。
51
-
52
- ### 加入 Discover 插件源
53
-
54
- 如果你的 DSH 配置启用了可选的插件市场 Discover,可添加这个自建索引地址:
55
-
56
- ```
57
- https://raw.githubusercontent.com/YinniCoding/momolulu/main/market.json
58
- ```
59
-
60
- 这是**维护者自建的索引**,不代表已进入某个中心化或官方市场。索引固定到发布 commit,尚未发布到 npm。
61
-
62
- ## 使用
63
-
64
- - 点击照片展开专注面板;倒计时旁的按钮可直接开始 / 暂停,不必展开面板。
65
- - 点 `−` 收成小头像,点小头像恢复照片。
66
- - 展开「提醒设置 → 测试提醒(8 秒)」,可预览晃动与高亮,不影响真实提醒时间。
67
- - 点雷达摘要或小头像角标查看其他会话;点整行进入对应对话。
68
- - 展开「外观设置」可切换场景、动效、阅读保护,调整背景亮度与浓度。
69
- - 暂停、计时结束或插件卸载都不会强制退出全屏;只有 Esc 或「退出全屏」会退出。
37
+ > 换装前请先停用旧的 `@local/lumululu-mei`,两者会同时渲染。
70
38
 
71
39
  ## 隐私
72
40
 
73
- 见 [PRIVACY.md](PRIVACY.md)。插件没有开发者后台,也没有插件统计;天气是第三方请求,偏好与计时保存在浏览器本地,定位可选。
74
-
75
- ## 常见问题
76
-
77
- **为什么同时装了 momolulu 和 lumululu-mei 会出问题?**
78
- 两者是同一个插件的前后版本,会同时渲染同一个陪伴组件。启用 `momolulu` 前请先停用或卸载 `@local/lumululu-mei`。
79
-
80
- **天气为什么默认是南京?**
81
- 默认坐标固定为南京(32.06, 118.80)。点「使用当前位置」后才会请求定位,刷新页面会回到南京。
82
-
83
- **开始专注为什么要全屏?**
84
- 这是一个可选仪式感。全屏失败不影响计时;按 Esc 只退出全屏,计时继续。
85
-
86
- **支持哪些 DSH 版本?**
87
- 声明支持 `>=0.2.1-rc.5 <0.3.0`;主要在 `0.2.2-rc.1` 上验证。
88
-
89
- **数据存在哪里?**
90
- 偏好与计时存在浏览器 `localStorage` 的 `llm.companion.v2`,不上传;会话雷达只读取 DSH 本地状态。
91
-
92
- **怎么卸载?**
93
- 用 DSH 插件管理器卸载 `momolulu`。需要清空偏好时,再手动删除该 localStorage 项。
41
+ 偏好和计时只存在浏览器 `localStorage`,不上传;雷达只读本地会话状态;唯一的网络请求是天气。详见 [PRIVACY.md](PRIVACY.md)。
94
42
 
95
43
  ## 已知限制
96
44
 
97
- - 雷达只列普通会话,不含子代理会话。「刚结束」不代表执行成功;待查看状态仅在当前页面保留,刷新后可能丢失。
98
- - 多个窗口之间不共享专注计时状态。
99
- - 浏览器或 Electron 可能拒绝全屏或定位,这类失败不会阻止计时。
100
- - 计时与提醒不是系统级闹钟:关闭 DSH 或设备休眠时不会准时触发,恢复后会按截止时间追赶。
101
- - 天气默认南京;通过定位取得的坐标保留两位小数,且只存在内存中。
102
- - 自动化测试覆盖计时逻辑、雷达分类与提醒判定、全屏控制(注入测试端口)。跨平台视觉布局、真实全屏、会话跳转与权限事件仍需手动验证。
103
- - 未内置自动更新;升级请按 DSH 插件管理器支持的方式卸载后重装。
45
+ 只支持普通会话(不含子代理);多窗口不同步计时;计时不是系统闹钟,关掉 DSH 就不会准时触发;未内置自动更新。
104
46
 
105
47
  ## 开发
106
48
 
107
- 本地检查需要 Node.js 20 及以上;无需构建步骤,也没有安装脚本。
108
-
109
49
  ```sh
110
- npm run check
111
- npm test
112
- npm pack --dry-run
50
+ npm run check && npm test
113
51
  ```
114
52
 
115
- ## 开源许可
116
-
117
- - **代码**:[MIT](LICENSE)。
118
- - **图片素材**(三张场景图、图标与截图):[CC BY 4.0](LICENSE-ARTWORK.md),署名「momolulu artwork © 2026 YinniCoding,CC BY 4.0」。
53
+ ## 许可
119
54
 
120
- 天气数据来自 [Open-Meteo](https://open-meteo.com/)(CC BY 4.0)。与 DeepSeek 及角色权利方无隶属关系,详见 [NOTICE.md](NOTICE.md)。
55
+ 代码 [MIT](LICENSE);图片素材 [CC BY 4.0](LICENSE-ARTWORK.md)。天气数据来自 [Open-Meteo](https://open-meteo.com/),与 DeepSeek 无隶属关系。
package/client.js CHANGED
@@ -13,7 +13,7 @@ window.__ModuleLoader__.load({
13
13
  const zh = {
14
14
  title: 'momolulu', subtitle: '慢慢来,我陪你一起。', meadow: '草甸', hike: '徒步', lake: '湖畔',
15
15
  radarWaiting: '等待处理', radarApproval: '请求权限', radarQuestion: '等待输入', radarSoftHint: '有其他会话可查看,无需确认或立即处理。', radar: '会话雷达', radarRunning: '运行中', radarUnread: '刚结束 · 待查看', radarEmpty: '暂无运行中或待查看的会话', radarHint: '结束不代表成功;待查看状态仅在当前页面保留。', openFailed: '无法打开此会话,请从侧栏重试。', showAll: '展开全部', showLess: '收起列表', reminderPaused: '提醒随专注暂停',
16
- testReminder: '测试提醒(8 秒)', minimize: '缩成小头像', restore: '恢复照片', details: '详情',
16
+ testReminder: '测试提醒(8 秒)', sound: '提示音', soundOn: '提示音 · 开', soundOff: '提示音 · 关', focusCommand: '专注', focusCommandHint: '开始或暂停 momolulu 的专注计时', minimize: '缩成小头像', restore: '恢复照片', details: '详情',
17
17
  close: '收起', focus: '专注倒计时', start: '开始专注', pause: '暂停', resume: '继续', reset: '重置',
18
18
  ready: '准备好,开始一小段专注', running: '专注中', paused: '已暂停', done: '这一轮完成啦',
19
19
  minutes: '分钟', elapsed: '已专注', reminders: '休息提醒', on: '开启', off: '关闭', next: '距离下次休息',
@@ -29,7 +29,7 @@ window.__ModuleLoader__.load({
29
29
  const en = {
30
30
  title: 'momolulu', subtitle: 'One step at a time, together.', meadow: 'Meadow', hike: 'Hike', lake: 'Lake',
31
31
  radarWaiting: 'Needs input', radarApproval: 'Permission requested', radarQuestion: 'Waiting for input', radarSoftHint: 'Other sessions have updates. No acknowledgement or immediate action required.', radar: 'Session radar', radarRunning: 'Running', radarUnread: 'Ended · unread', radarEmpty: 'No running or unread sessions', radarHint: 'Ended does not mean successful. Unread status is local to this page.', openFailed: 'Unable to open this session. Try the sidebar.', showAll: 'Show all', showLess: 'Show less', reminderPaused: 'Reminders paused with focus',
32
- testReminder: 'Test reminder (8 sec)', minimize: 'Minimize to avatar', restore: 'Restore photo', details: 'Details',
32
+ testReminder: 'Test reminder (8 sec)', sound: 'Chime', soundOn: 'Chime · On', soundOff: 'Chime · Off', focusCommand: 'Focus', focusCommandHint: 'Start or pause the momolulu focus timer', minimize: 'Minimize to avatar', restore: 'Restore photo', details: 'Details',
33
33
  close: 'Close', focus: 'Focus countdown', start: 'Start focus', pause: 'Pause', resume: 'Resume', reset: 'Reset',
34
34
  ready: 'Ready for a little focus time', running: 'Focusing', paused: 'Paused', done: 'Session complete',
35
35
  minutes: 'min', elapsed: 'Focused', reminders: 'Break reminders', on: 'On', off: 'Off', next: 'Next break in',
@@ -93,6 +93,7 @@ window.__ModuleLoader__.load({
93
93
  minutes, status: valid ? saved.status : 'ready', remaining: valid ? saved.remaining : total,
94
94
  deadline: valid ? saved.deadline : 0, now: Date.now(),
95
95
  reminderRemaining: Number.isFinite(saved?.reminderRemaining) ? Math.max(0, saved.reminderRemaining) : 20 * 60000,
96
+ sound: saved?.sound === true,
96
97
  reminders: saved?.reminders !== false, nextReminder: Number.isFinite(saved?.nextReminder) ? saved.nextReminder : 0,
97
98
  pausedDate: typeof saved?.pausedDate === 'string' ? saved.pausedDate : '', notice: '', noticeAt: 0,
98
99
  scene: [0, 1, 2].includes(saved?.scene) ? saved.scene : 0,
@@ -112,15 +113,49 @@ window.__ModuleLoader__.load({
112
113
  if (persist) storage.save(state);
113
114
  for (const fn of listeners) fn();
114
115
  }
116
+ // Optional chime. The AudioContext is created only after a user gesture, so
117
+ // browsers that block autoplay simply stay silent instead of erroring.
118
+ let audioContext;
119
+ function ensureAudio() {
120
+ try {
121
+ const Ctor = window.AudioContext || window.webkitAudioContext;
122
+ if (!Ctor) return undefined;
123
+ audioContext ??= new Ctor();
124
+ if (audioContext.state === 'suspended') void audioContext.resume().catch(() => {});
125
+ } catch { audioContext = undefined; }
126
+ return audioContext;
127
+ }
128
+ function playChime() {
129
+ const audio = ensureAudio();
130
+ if (!audio || audio.state !== 'running') return;
131
+ const start = audio.currentTime;
132
+ [660, 880].forEach((frequency, index) => {
133
+ const oscillator = audio.createOscillator();
134
+ const gain = audio.createGain();
135
+ const at = start + index * 0.18;
136
+ oscillator.type = 'sine';
137
+ oscillator.frequency.value = frequency;
138
+ gain.gain.setValueAtTime(0, at);
139
+ gain.gain.linearRampToValueAtTime(0.12, at + 0.02);
140
+ gain.gain.exponentialRampToValueAtTime(0.0001, at + 0.35);
141
+ oscillator.connect(gain).connect(audio.destination);
142
+ oscillator.start(at);
143
+ oscillator.stop(at + 0.4);
144
+ });
145
+ }
115
146
  function tick(now = Date.now()) {
116
147
  const patch = { now };
117
148
  let persist = false;
118
149
  if (state.status === 'running' && remainingMs(state, now) === 0) {
119
150
  Object.assign(patch, { status: 'done', remaining: 0 });
120
- if (state.reminders && state.pausedDate !== new Date(now).toDateString()) Object.assign(patch, { notice: 'finish', noticeAt: now });
151
+ if (state.reminders && state.pausedDate !== new Date(now).toDateString()) {
152
+ Object.assign(patch, { notice: 'finish', noticeAt: now });
153
+ if (state.sound) playChime();
154
+ }
121
155
  persist = true;
122
156
  } else if (state.status === 'running' && state.reminders && state.pausedDate !== new Date(now).toDateString() && state.nextReminder && now >= state.nextReminder) {
123
157
  Object.assign(patch, { notice: 'break', noticeAt: now, nextReminder: now + 20 * 60000 });
158
+ if (state.sound) playChime();
124
159
  persist = true;
125
160
  }
126
161
  update(patch, persist);
@@ -323,7 +358,7 @@ window.__ModuleLoader__.load({
323
358
  rows.slice(0, showAll ? rows.length : 3).map((row) => button(
324
359
  h(React.Fragment, null, h('span', { className: 'llm-radar-row-title' }, row.displayTitle || row.id, row.attentionKind && h('small', { className: 'llm-radar-kind' }, t(row.attentionKind))), h('span', { 'aria-hidden': true }, '↗')),
325
360
  () => navigate(row), { key: row.id, className: 'llm-radar-row', title: `${row.displayTitle || row.id}${row.cwd ? '\n' + row.cwd : ''}` })));
326
- return h('div', { className: `llm-root ${panelMode ? 'llm-panel' : 'llm-floating'}` },
361
+ return h('div', { className: `llm-root ${panelMode ? 'llm-panel' : 'llm-floating'}`, onPointerDownCapture: () => { if (model.sound) ensureAudio(); } },
327
362
  h('style', null, css),
328
363
  model.bgOn && h('div', { className: 'llm-bg' + (model.readingProtection && !panelMode ? ' llm-bg-protected' : ''), style: { backgroundImage: `url("${asset(currentScene.name)}")`, opacity: model.intensity / 100, filter: `brightness(${model.brightness / 100}) saturate(1.12)` } }),
329
364
  !panelMode && h('button', { type: 'button', ref: radarTrigger, className: 'llm-radar-trigger' + (radarHighlight ? ' llm-radar-attention' : '') + (radarShake ? ' llm-alert-shake' : '') + (model.minimized && !expanded ? ' llm-radar-badge' : ''), onClick: () => { setRadarOpen(!radarOpen); setRadarAttentionAt(0); }, 'aria-expanded': radarOpen, 'aria-controls': radarId, 'aria-label': `${t('radar')} · ${radarSummary}`, title: radarSummary + (radarHighlight ? ' · ' + t('radarSoftHint') : '') }, model.minimized && !expanded ? (radarCount > 99 ? '99+' : radarCount || '☷') : `☷ ${radarSummary}`),
@@ -366,6 +401,7 @@ window.__ModuleLoader__.load({
366
401
  h('div', { className: 'llm-sub', style: { marginTop: 10 } }, `${t('elapsed')} · ${formatTime(total - remaining)}`)),
367
402
  h('details', { className: 'llm-disclosure' }, h('summary', null, `${t('reminderSettings')} · ${t(model.reminders && !todayPaused ? 'on' : 'off')}`),
368
403
  h('div', { className: 'llm-settings' }, button(t(model.reminders && !todayPaused ? 'on' : 'off'), () => setReminders(!model.reminders || todayPaused), { role: 'switch', 'aria-checked': model.reminders && !todayPaused })),
404
+ h('div', { className: 'llm-settings' }, button(t(model.sound ? 'soundOn' : 'soundOff'), () => { update({ sound: !model.sound }); if (!model.sound) ensureAudio(); }, { role: 'switch', 'aria-checked': model.sound })),
369
405
  h('div', { className: 'llm-summary' }, t('reminderEvery')),
370
406
  button(t('testReminder'), () => { const now = Date.now(); update({ now, previewAt: now, previewUntil: now + 8000 }, false); setOpen(false); if (panelMode) layout.selectPanel(null); }),
371
407
  h('div', { className: 'llm-summary' }, reminderLabel(model))),
@@ -419,6 +455,17 @@ window.__ModuleLoader__.load({
419
455
  document.addEventListener('fullscreenchange', onChange);
420
456
  return () => { document.removeEventListener('fullscreenchange', onChange); control.dispose(); if (fullscreenControl === control) fullscreenControl = undefined; };
421
457
  }, 'lumululu-mei fullscreen lifecycle');
458
+ ctx.inject(['commandUi'], (scope) => {
459
+ const commandUi = scope.get('commandUi');
460
+ if (commandUi === undefined) return;
461
+ scope.effect(() => commandUi.register({
462
+ name: 'focus',
463
+ label: () => t('focusCommand'),
464
+ description: () => t('focusCommandHint'),
465
+ available: () => true,
466
+ ui: { kind: 'action', run: () => { if (state.status === 'running') pause(); else startFocused(); } },
467
+ }), 'momolulu /focus command');
468
+ });
422
469
  ctx.slots.inject('sidebar.panellist', () => ctx.slots.register({ name: 'sidebar.panellist', id: 'lumululu-mei', order: 18, label: () => t('title') }, Icon));
423
470
  ctx.slots.inject('main', () => ctx.slots.register({ name: 'main', key: 'lumululu-mei' }, (props) => h(Companion, { ...props, panelMode: true })));
424
471
  ctx.slots.inject('shell.overlay', () => ctx.slots.register({ name: 'shell.overlay', id: 'lumululu-mei-overlay', order: 100, label: () => t('title') }, Companion));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "momolulu",
3
- "version": "1.0.17",
3
+ "version": "1.1.0",
4
4
  "type": "module",
5
5
  "description": "momolulu, a gentle coding companion with scenic backgrounds, live session activity, focus timing and wellbeing nudges.",
6
6
  "exports": {