dsh-island 0.4.0 → 0.6.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.md +56 -11
- package/desktop/index.html +18 -0
- package/desktop/island.css +44 -0
- package/desktop/main.cjs +77 -9
- package/desktop/preload.cjs +5 -0
- package/desktop/renderer.js +136 -18
- package/desktop/stats-preload.cjs +5 -0
- package/desktop/stats.css +86 -0
- package/desktop/stats.html +62 -0
- package/desktop/stats.js +147 -0
- package/package.json +2 -2
- package/src/approvals.js +89 -0
- package/src/chat.js +66 -0
- package/src/host.js +51 -7
- package/src/pricing.js +42 -0
- package/src/settings.js +5 -2
- package/src/state.js +10 -4
- package/src/usage.js +181 -0
- package/web/client.js +16 -1
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# DeepSeek Island · 桌面灵动岛
|
|
2
2
|
|
|
3
|
-
DeepSeek Harness 的桌面灵动岛插件:像 iPhone 灵动岛一样停在屏幕边缘,实时显示 Agent
|
|
3
|
+
DeepSeek Harness 的桌面灵动岛插件:像 iPhone 灵动岛一样停在屏幕边缘,实时显示 Agent 在做什么——思考、回答、执行工具、等你确认;展开后是任务控制台,可以看计划进度、上下文占用和用量、停止任务,直接批准权限请求、回答 Agent 的提问,任务完成时预览回复;另有按时间、模型、会话、轮次统计的用量窗口,并按 DeepSeek 官方价格估算费用。
|
|
4
4
|
|
|
5
5
|
> 社区插件,非 DeepSeek 官方产品。DeepSeek 名称与鲸鱼标志归其所有者。
|
|
6
6
|
|
|
@@ -10,6 +10,12 @@ DeepSeek Harness 的桌面灵动岛插件:像 iPhone 灵动岛一样停在屏
|
|
|
10
10
|
| --- | --- |
|
|
11
11
|
|  |  |
|
|
12
12
|
|
|
13
|
+
| 批准权限请求 | 有新回复 |
|
|
14
|
+
| --- | --- |
|
|
15
|
+
|  |  |
|
|
16
|
+
|
|
17
|
+

|
|
18
|
+
|
|
13
19
|

|
|
14
20
|
|
|
15
21
|
[动画录屏(mp4)](docs/motion.mp4)
|
|
@@ -30,16 +36,19 @@ DeepSeek Harness 的桌面灵动岛插件:像 iPhone 灵动岛一样停在屏
|
|
|
30
36
|
| --- | --- | --- |
|
|
31
37
|
| 待机 | 118 × 32 | 没有任务:只有鲸鱼和一颗呼吸的蓝点 |
|
|
32
38
|
| 紧凑 | 212 × 36 | 任务进行中:鲸鱼 · 状态文字 · 动态指示器,多任务时显示数量 |
|
|
33
|
-
| 弹出 | 312 × 66 |
|
|
34
|
-
| 展开 | 344 × 自适应 |
|
|
39
|
+
| 弹出 | 312 × 66 | 新任务开始、等待确认、完成、异常时自动弹出几秒,显示任务标题;完成时显示回复开头 |
|
|
40
|
+
| 展开 | 344 × 自适应 | 鼠标停留约 0.7 秒或单击:任务控制台 |
|
|
35
41
|
| 竖向 | 32 × 78 / 36 × 104 | 停靠在左侧或右侧居中时变为竖条,悬停或提醒时横向舒展 |
|
|
36
42
|
|
|
37
43
|
尺寸、圆角、鲸鱼和指示器的位置都随形态一起用带轻微回弹的弹簧曲线过渡;状态切换时文字和图标做模糊交叉淡入。
|
|
38
44
|
|
|
45
|
+
任务进行中,鲸鱼外圈是**上下文占用环**:按当前上下文占模型窗口的比例描边,70% 以下绿色,70% 起黄色,90% 起红色并轻轻呼吸,提示快要整理上下文了。
|
|
46
|
+
|
|
39
47
|
## 使用
|
|
40
48
|
|
|
41
|
-
-
|
|
42
|
-
-
|
|
49
|
+
- 鼠标在灵动岛上停留片刻即展开任务控制台,移开后自动收起;悬停展开后再单击可固定,不随鼠标离开收起。可在设置中关闭悬停展开。
|
|
50
|
+
- 单击胶囊展开或收起,按 Esc 或点击灵动岛以外的任何地方收起;单击展开的面板在鼠标离开 6 秒后也会自动收起。提问卡里没提交的草稿会保留,重新展开可以接着答。
|
|
51
|
+
- “打开 Harness”回到主窗口;面板底部的柱状图按钮或托盘菜单「用量统计」打开用量窗口。
|
|
43
52
|
- “隐藏”只隐藏灵动岛,点系统托盘的蓝色鲸鱼可重新显示;右键托盘可选显示器或退出。
|
|
44
53
|
- 透明区域不拦截鼠标;窗口置顶但出现时不抢输入焦点。
|
|
45
54
|
|
|
@@ -47,20 +56,53 @@ DeepSeek Harness 的桌面灵动岛插件:像 iPhone 灵动岛一样停在屏
|
|
|
47
56
|
|
|
48
57
|
| 什么时候 | 显示什么 |
|
|
49
58
|
| --- | --- |
|
|
50
|
-
| 有任务进行中 | 任务标题与计时;Agent 计划的进度条(如 4/7
|
|
51
|
-
| 刚结束 |
|
|
52
|
-
| 空闲 | 今天完成的任务数、工作时长、DeepSeek 账户余额(未登录账号时显示今日 tokens);过去 24 小时每小时的 token
|
|
59
|
+
| 有任务进行中 | 任务标题与计时;Agent 计划的进度条(如 4/7)和当前步骤;最近几步工具(读取文件、编辑、终端…);模型、上下文占用(如 412K / 1.0M (39%))、缓存命中率、工具次数、输出 tokens;其他同时进行的任务;**停止任务**按钮 |
|
|
60
|
+
| 刚结束 | **有新回复**:Agent 最后一条回复的开头(点击回到 Harness 看全文);结果、最终计划进度、本次用量 |
|
|
61
|
+
| 空闲 | 今天完成的任务数、工作时长、DeepSeek 账户余额(未登录账号时显示今日 tokens);过去 24 小时每小时的 token 柱状图、缓存命中率和今日估算费用;最近几个任务(结果、时长、多久以前) |
|
|
53
62
|
|
|
54
|
-
**停止任务**要点两次(第一次变红确认),效果与 Harness
|
|
63
|
+
**停止任务**要点两次(第一次变红确认),效果与 Harness 里的停止按钮相同,只对顶层任务生效;子智能体不会单独列为任务,除非它在等你确认或出错。用量数据来自 Harness 自己的会话记录(`ctx.sessionQuery`):启用后在后台补算最近 30 天的会话,之后跟随实时事件更新,汇总保存在 `%USERPROFILE%\.dsh\dsh-island\usage.json`,只含会话标题、每轮的模型、时间、结果、调用与工具次数、token 数和估算费用,保留 35 天,不含对话内容。
|
|
55
64
|
|
|
56
65
|
账户余额通过 Harness 自己的账户服务读取(与「设置 › 账户」相同),只在用 DeepSeek 账号登录时可用;灵动岛看不到任何令牌或 API Key。打开面板时最多每分钟刷新一次,另外每 10 分钟刷新一次。
|
|
57
66
|
|
|
67
|
+
## 在灵动岛给 Agent 发消息
|
|
68
|
+
|
|
69
|
+
展开面板顶部是对话卡:上面是这个任务最近一次的回复(最多 4000 字,可滚动、可复制),下面是输入框,**不用打开 Harness 就能接着聊**。Enter 发送,Shift+Enter 换行,中文输入法选字时按 Enter 不会误发。
|
|
70
|
+
|
|
71
|
+
- 发给谁:正在显示的任务;空闲时发给最近一个任务。子智能体不会成为对象。
|
|
72
|
+
- 空闲时点 **发送** 开始新一轮;任务进行中点 **排队**(这一轮结束后发送),或点 **插话**(插入正在进行的这一轮,Agent 下一步就会看到)。
|
|
73
|
+
- 走的是 Harness 自己输入框用的同一个接口(会话控制器的 `prompt`),消息会出现在 Harness 的对话记录里;已关闭的任务会被自动恢复。没有选好模型等原因发送失败时,输入框里的内容保留,可以直接重试。
|
|
74
|
+
- 暂不支持新建对话和发送图片、文件,这些仍在 Harness 中操作。可在设置中关闭「在灵动岛给 Agent 发消息」。
|
|
75
|
+
|
|
58
76
|
## 在灵动岛回答提问
|
|
59
77
|
|
|
60
78
|
Agent 用 `ask_user_question` 提问时,灵动岛自动展开提问卡:单选、多选、自己输入、跳过都可以,多个问题逐个回答后一次提交。Harness 窗口里的提问卡照常可用,**哪边先答用哪边**;灵动岛先答时,Harness 里的那张卡会随之关闭。没有打开 Harness 窗口时,灵动岛就是回答入口。问题超时后转为“继续”状态,仍可在灵动岛回答,回答作为一条回复送回 Agent,与 Harness 的处理方式相同。
|
|
61
79
|
|
|
62
80
|
计划审阅(plan mode)和带长说明的问题需要看全文,仍在 Harness 中回答。可在设置中关闭「在灵动岛回答提问」。
|
|
63
81
|
|
|
82
|
+
## 在灵动岛批准权限请求
|
|
83
|
+
|
|
84
|
+
Agent 要执行需要授权的操作(运行命令、修改文件、访问网址等)时,灵动岛变为“需要你批准”并自动展开审批卡,显示是哪个工具、为什么、具体要做什么(命令全文、文件路径或网址)。可以 **允许一次** 或 **拒绝**;按钮在卡片出现后约 0.6 秒才可点,避免误触。Harness 窗口里的审批卡照常可用,**哪边先批用哪边**,灵动岛先批时 Harness 里的卡随之撤下;没有打开 Harness 窗口时灵动岛就是审批入口。多个请求依次处理。
|
|
85
|
+
|
|
86
|
+
“始终允许”等持久授权不在灵动岛提供,仍在 Harness 中设置;子智能体的请求也在 Harness 中处理。可在设置中关闭「在灵动岛批准权限请求」。
|
|
87
|
+
|
|
88
|
+
## 用量统计
|
|
89
|
+
|
|
90
|
+
从面板底部的柱状图按钮或托盘菜单打开,范围可选 **今天**(按小时)、**7 天**、**30 天**(按天):
|
|
91
|
+
|
|
92
|
+
- 概览:总 tokens、估算费用、缓存命中率、轮次(完成/其他)、工作时长。
|
|
93
|
+
- 堆叠柱状图:缓存命中 / 未命中输入 / 输出,悬停查看每段的数量、费用和轮次。
|
|
94
|
+
- 按模型:tokens、估算费用、轮次。
|
|
95
|
+
- 按会话:每个会话的轮次、tokens、费用,点开看每一轮的结束时间、模型、调用与工具次数、各类 tokens 和费用。
|
|
96
|
+
|
|
97
|
+
费用按 [DeepSeek 官方价格](https://api-docs.deepseek.com/zh-cn/quick_start/pricing)(2026-10-27 查询,人民币/百万 tokens)逐次调用估算:
|
|
98
|
+
|
|
99
|
+
| 模型 | 时段 | 缓存命中输入 | 未命中输入 | 输出 |
|
|
100
|
+
| --- | --- | --- | --- | --- |
|
|
101
|
+
| deepseek-flash | 空闲 / 高峰 | 0.02 / 0.04 | 1 / 2 | 4 / 8 |
|
|
102
|
+
| deepseek-v4-pro | 空闲 / 高峰 | 0.15 / 0.30 | 4.5 / 9 | 13.5 / 27 |
|
|
103
|
+
|
|
104
|
+
高峰时段为北京时间工作日 9:00–12:00、14:00–18:00(法定节假日无法识别,按工作日计)。写入缓存的输入按未命中价,输出含思考内容;已下线的 `deepseek-v4-flash` 按 Flash 价计;没有官方价格的模型不计入费用。估算仅供参考,实际扣费以 DeepSeek 账单为准。
|
|
105
|
+
|
|
64
106
|
## 在 Harness 中设置
|
|
65
107
|
|
|
66
108
|
位置、大小等选项在 Harness 的 **设置 › 灵动岛** 中,修改后立即生效,无需重启:
|
|
@@ -71,8 +113,11 @@ Agent 用 `ask_user_question` 提问时,灵动岛自动展开提问卡:单
|
|
|
71
113
|
| 显示灵动岛 | 关闭即隐藏;打开时会重新显示 |
|
|
72
114
|
| 大小 | 小巧、适中、舒展 |
|
|
73
115
|
| 弹出提醒 | 任务开始、完成、需要确认或出错时短暂放大 |
|
|
74
|
-
| 显示任务内容 |
|
|
116
|
+
| 显示任务内容 | 关闭后不显示任务标题、计划内容、工具名、审批详情和回复预览,只显示状态 |
|
|
75
117
|
| 在灵动岛回答提问 | Agent 提问时自动展开提问卡,可直接回答 |
|
|
118
|
+
| 在灵动岛批准权限请求 | 需要授权时显示审批卡,可允许一次或拒绝 |
|
|
119
|
+
| 在灵动岛给 Agent 发消息 | 面板里显示输入框,接着当前或最近的任务对话 |
|
|
120
|
+
| 悬停展开面板 | 鼠标停留即展开,移开收起;关闭后需要单击 |
|
|
76
121
|
| 显示账户余额 | 空闲时显示 DeepSeek 账户余额 |
|
|
77
122
|
| 减少动态效果 | 关闭循环动画和弹性过渡(也跟随系统设置) |
|
|
78
123
|
|
|
@@ -126,7 +171,7 @@ npm run preview
|
|
|
126
171
|
|
|
127
172
|
## 开发与验证
|
|
128
173
|
|
|
129
|
-
代码无需打包器和运行时 npm 依赖。`src/host.js` 使用 Cordis 生命周期及 `session/event`、`agent/assistant-stream`、`agent/status`、`agent/error`,通过父子进程 IPC 向隔离的 Electron 页面发送状态;`src/settings.js` 用 Harness 自带的 schemastery 声明 volatile 配置,`loader/volatile-update` 时把新设置转发给窗口;`web/client.js` 是 Harness 设置页的浏览器半侧(通过 `dsh.client` 声明加载)。传给灵动岛的只有状态、任务标题、Agent 计划条目、工具名、模型名、token
|
|
174
|
+
代码无需打包器和运行时 npm 依赖。`src/host.js` 使用 Cordis 生命周期及 `session/event`、`agent/assistant-stream`、`agent/status`、`agent/error`,通过父子进程 IPC 向隔离的 Electron 页面发送状态;`src/settings.js` 用 Harness 自带的 schemastery 声明 volatile 配置,`loader/volatile-update` 时把新设置转发给窗口;`web/client.js` 是 Harness 设置页的浏览器半侧(通过 `dsh.client` 声明加载)。传给灵动岛的只有状态、任务标题、Agent 计划条目、工具名、模型名、token 数、账户余额、等你回答的问题和选项,以及已完成一轮的最终回复(关闭「显示任务内容」后不传);不会读取密钥,也不传递你的提问、思考正文、进行中的文本或工具参数。你在灵动岛输入的消息经 IPC 交给 Harness 的会话控制器,灵动岛本身不保存。没有新增 HTTP 监听端口。加载时接管正在运行任务的概括状态,之后使用实时事件细分;不重放历史对话。
|
|
130
175
|
|
|
131
176
|
```powershell
|
|
132
177
|
npm test
|
package/desktop/index.html
CHANGED
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
<div class="surface">
|
|
12
12
|
<button id="capsule" class="capsule" aria-label="展开 DeepSeek 灵动岛" aria-expanded="false" aria-controls="panel">
|
|
13
13
|
<span class="lead" aria-hidden="true">
|
|
14
|
+
<svg class="ctx-ring" viewBox="0 0 40 40" aria-hidden="true"><circle class="ctx-track" cx="20" cy="20" r="18"/><circle id="ctx-arc" class="ctx-arc" cx="20" cy="20" r="18" pathLength="100"/></svg>
|
|
14
15
|
<span class="whale"></span>
|
|
15
16
|
<span class="spout"><i></i><i></i><i></i></span>
|
|
16
17
|
</span>
|
|
@@ -35,6 +36,16 @@
|
|
|
35
36
|
</button>
|
|
36
37
|
|
|
37
38
|
<section id="panel" class="panel" inert aria-label="任务详情">
|
|
39
|
+
<form id="approve" class="card ask approve" hidden autocomplete="off">
|
|
40
|
+
<div class="ask-top"><span class="ask-dot" aria-hidden="true"></span><span id="approve-title">需要你批准</span><span id="approve-count" class="num"></span></div>
|
|
41
|
+
<p id="approve-reason" class="ask-q"></p>
|
|
42
|
+
<div id="approve-detail" class="approve-detail"><span id="approve-label"></span><code id="approve-text"></code></div>
|
|
43
|
+
<div class="ask-actions"><span id="approve-status" class="ask-status" role="status"></span><button id="approve-reject" type="button" class="ask-skip approve-reject">拒绝</button><button id="approve-allow" type="submit" class="ask-next">允许一次</button></div>
|
|
44
|
+
</form>
|
|
45
|
+
<div id="reply" class="card reply" hidden>
|
|
46
|
+
<div class="reply-head"><span class="reply-dot" aria-hidden="true"></span>有新回复<span class="reply-hint">回到 Harness 查看全文</span></div>
|
|
47
|
+
<p id="reply-text"></p>
|
|
48
|
+
</div>
|
|
38
49
|
<form id="ask" class="card ask" hidden autocomplete="off">
|
|
39
50
|
<div class="ask-top"><span class="ask-dot" aria-hidden="true"></span><span id="ask-title">Agent 在问你</span><span id="ask-step" class="num"></span></div>
|
|
40
51
|
<p id="ask-question" class="ask-q"></p>
|
|
@@ -42,6 +53,12 @@
|
|
|
42
53
|
<input id="ask-custom" class="ask-custom" type="text" maxlength="2000" placeholder="或者输入你自己的回答…" aria-label="自定义回答">
|
|
43
54
|
<div class="ask-actions"><span id="ask-status" class="ask-status" role="status"></span><button id="ask-skip" type="button" class="ask-skip">跳过</button><button id="ask-next" type="submit" class="ask-next">提交</button></div>
|
|
44
55
|
</form>
|
|
56
|
+
<form id="chat" class="card chat" hidden autocomplete="off">
|
|
57
|
+
<div class="chat-top"><span class="chat-dot" aria-hidden="true"></span><span id="chat-title">继续对话</span><button id="chat-open" type="button" class="chat-link">在 Harness 中查看 ↗</button></div>
|
|
58
|
+
<div id="chat-reply" class="chat-reply" tabindex="0" aria-label="最近的回复" hidden></div>
|
|
59
|
+
<textarea id="chat-input" class="chat-input" rows="2" maxlength="8000" placeholder="给 Agent 发消息… Enter 发送,Shift+Enter 换行" aria-label="给 Agent 发消息"></textarea>
|
|
60
|
+
<div class="ask-actions"><span id="chat-status" class="ask-status" role="status"></span><button id="chat-steer" type="button" class="ask-skip" title="插入正在进行的这一轮,Agent 下一步就会看到" hidden>插话</button><button id="chat-send" type="submit" class="ask-next chat-send">发送</button></div>
|
|
61
|
+
</form>
|
|
45
62
|
<div id="task" class="card task">
|
|
46
63
|
<div class="task-top"><span id="session-title">DeepSeek Harness</span><time id="elapsed"></time></div>
|
|
47
64
|
<p id="status-detail">开始一个任务,让想法继续。</p>
|
|
@@ -82,6 +99,7 @@
|
|
|
82
99
|
<div class="footer">
|
|
83
100
|
<button id="stop" class="stop" hidden><span class="stop-glyph" aria-hidden="true"></span><span id="stop-label">停止任务</span></button>
|
|
84
101
|
<button id="open-harness" class="open-harness">打开 Harness <span aria-hidden="true">↗</span></button>
|
|
102
|
+
<button id="stats" class="icon-button" aria-label="用量统计" title="用量统计"><svg viewBox="0 0 16 16" aria-hidden="true" width="14" height="14"><path d="M2.5 13.5h11M4.5 11V7.5M8 11V3.5M11.5 11V6" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round"/></svg></button>
|
|
85
103
|
<button id="more" class="icon-button" aria-label="更多" title="显示器、隐藏、退出">···</button>
|
|
86
104
|
<button id="hide" class="hide" title="可从系统托盘重新显示">隐藏</button>
|
|
87
105
|
</div>
|
package/desktop/island.css
CHANGED
|
@@ -281,6 +281,43 @@ p{margin:3px 0 0;font-size:11.5px;line-height:1.5;color:var(--muted)}
|
|
|
281
281
|
.ask-next{height:28px;padding:0 14px;border-radius:9px;font-size:11.5px;font-weight:600;color:#1b1203;background:#f5b544}
|
|
282
282
|
.ask-next:hover{background:#ffc35c}
|
|
283
283
|
.ask-next:disabled{opacity:.45;cursor:default}
|
|
284
|
+
.ctx-ring{position:absolute;inset:-5px;width:calc(100% + 10px);height:calc(100% + 10px);transform:rotate(-90deg);opacity:0;transition:opacity .4s;pointer-events:none;overflow:visible}
|
|
285
|
+
.ctx-track{fill:none;stroke:rgba(255,255,255,.08);stroke-width:2.6}
|
|
286
|
+
.ctx-arc{fill:none;stroke:var(--ctx-color,#3ddc91);stroke-width:2.6;stroke-linecap:round;stroke-dasharray:var(--ctx-pct,0) 100;transition:stroke-dasharray .8s var(--spring),stroke .4s}
|
|
287
|
+
.island:not([data-state="idle"]):not([data-state="offline"])[data-ctx] .ctx-ring{opacity:1}
|
|
288
|
+
.island[data-ctx="ok"]{--ctx-color:#3ddc91}.island[data-ctx="warn"]{--ctx-color:#f5b544}.island[data-ctx="full"]{--ctx-color:#ff6b6b}
|
|
289
|
+
.island[data-ctx="full"] .ctx-arc{animation:pulse 1.6s ease-in-out infinite}
|
|
290
|
+
.approve{border-color:color-mix(in srgb,#ff9b5e 34%,transparent);background:linear-gradient(180deg,rgba(255,155,94,.08),rgba(255,255,255,.03))}
|
|
291
|
+
.approve .ask-dot{background:#ff9b5e;--c:#ff9b5e}
|
|
292
|
+
.approve .ask-top{color:#ffc39b}
|
|
293
|
+
.approve-detail{display:flex;flex-direction:column;gap:3px;padding:8px 10px;border-radius:10px;background:rgba(0,0,0,.4);border:1px solid rgba(255,255,255,.06)}
|
|
294
|
+
.approve-detail:empty,.approve-detail[hidden]{display:none}
|
|
295
|
+
.approve-detail #approve-label{font-size:10px;color:var(--muted)}
|
|
296
|
+
.approve-detail code{font:11.5px/1.5 "Cascadia Code",Consolas,"Microsoft YaHei UI",monospace;color:#f3f5ff;white-space:pre-wrap;word-break:break-all;max-height:84px;overflow:auto}
|
|
297
|
+
.approve-reject{color:#ffb4b4}
|
|
298
|
+
.approve-reject:hover{background:rgba(255,107,107,.14)!important;color:#ffd0d0!important}
|
|
299
|
+
.reply{border-color:color-mix(in srgb,#3ddc91 26%,transparent)}
|
|
300
|
+
.reply-head{display:flex;align-items:center;gap:7px;font-size:11px;color:#9ff0c6}
|
|
301
|
+
.reply-hint{margin-left:auto;font-size:10px;color:var(--faint)}
|
|
302
|
+
.reply-dot{width:7px;height:7px;border-radius:50%;background:#3ddc91}
|
|
303
|
+
.chat{display:flex;flex-direction:column;gap:8px;margin:0;border-color:color-mix(in srgb,var(--ds-blue) 32%,transparent);background:linear-gradient(180deg,color-mix(in srgb,var(--ds-blue) 9%,transparent),rgba(255,255,255,.03))}
|
|
304
|
+
.chat-top{display:flex;align-items:center;gap:8px;font-size:11px;color:#b9c6ff}
|
|
305
|
+
.chat-top #chat-title{flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
|
|
306
|
+
.chat-dot{flex:none;width:7px;height:7px;border-radius:50%;background:var(--ds-blue)}
|
|
307
|
+
.chat-link{flex:none;height:20px;padding:0 4px;border-radius:6px;font-size:10px;color:var(--faint)}
|
|
308
|
+
.chat-link:hover{background:rgba(255,255,255,.06);color:#e5e9f7}
|
|
309
|
+
.chat-reply{margin:0;font-size:12px;line-height:1.55;color:#e8ecf8;white-space:pre-wrap;word-break:break-word;max-height:calc(1.55em * 5);overflow:auto;padding-right:4px;user-select:text;outline:none}
|
|
310
|
+
.chat-reply.live{color:var(--muted)}
|
|
311
|
+
.chat-reply::-webkit-scrollbar{width:4px}.chat-reply::-webkit-scrollbar-thumb{border-radius:2px;background:rgba(255,255,255,.18)}
|
|
312
|
+
.chat-input{box-sizing:border-box;display:block;width:100%;min-height:50px;max-height:96px;resize:none;padding:7px 10px;border-radius:10px;border:1px solid rgba(255,255,255,.1);background:rgba(0,0,0,.35);color:#f3f5ff;font:inherit;font-size:12px;line-height:1.5;outline:none;transition:border-color .2s,box-shadow .2s}
|
|
313
|
+
.chat-input::placeholder{color:#5f6780}
|
|
314
|
+
.chat-input:focus{border-color:color-mix(in srgb,var(--ds-blue) 70%,transparent);box-shadow:0 0 0 3px color-mix(in srgb,var(--ds-blue) 22%,transparent)}
|
|
315
|
+
.chat-input:disabled{opacity:.6}
|
|
316
|
+
.ask-next.chat-send{background:var(--ds-blue);color:#fff}
|
|
317
|
+
.ask-next.chat-send:hover{background:var(--ds-blue-hi)}
|
|
318
|
+
#reply-text{margin:6px 0 0;font-size:12px;line-height:1.55;color:#e8ecf8;display:-webkit-box;-webkit-line-clamp:3;-webkit-box-orient:vertical;overflow:hidden}
|
|
319
|
+
.ghost{height:30px;padding:0 10px;border-radius:10px;font-size:11.5px;color:var(--muted)}
|
|
320
|
+
.ghost:hover{background:rgba(255,255,255,.06);color:#e5e9f7}
|
|
284
321
|
.hint{margin:0 2px;font-size:10.5px;line-height:1.5;color:var(--faint)}
|
|
285
322
|
|
|
286
323
|
.footer{display:flex;align-items:center;gap:6px;margin-top:2px}
|
|
@@ -339,3 +376,10 @@ body[data-motion="reduced"] .ind-completed .tick{stroke-dashoffset:0}
|
|
|
339
376
|
*,*::before,*::after{animation:none!important;transition-duration:.08s!important;transition-delay:0s!important}
|
|
340
377
|
.ind-completed .tick{stroke-dashoffset:0}
|
|
341
378
|
}
|
|
379
|
+
.approve-detail code.plain{font-family:inherit}
|
|
380
|
+
.island[data-alert="approval"]{--tint:#ff9b5e}
|
|
381
|
+
.reply{cursor:pointer;transition:background-color .2s}
|
|
382
|
+
.reply:hover{background:rgba(61,220,145,.07)}
|
|
383
|
+
.footer button{white-space:nowrap}
|
|
384
|
+
#stats svg{display:block;margin:auto}
|
|
385
|
+
#stats+#more{margin-left:0}
|
package/desktop/main.cjs
CHANGED
|
@@ -11,7 +11,7 @@ const single = app.requestSingleInstanceLock();
|
|
|
11
11
|
if(!single) app.quit();
|
|
12
12
|
else start().catch(error=>{ console.error(error); app.exit(1); });
|
|
13
13
|
|
|
14
|
-
const WIDTH = 440, HEIGHT =
|
|
14
|
+
const WIDTH = 440, HEIGHT = 540;
|
|
15
15
|
const SIZES = { small: 0.85, medium: 1, large: 1.15 };
|
|
16
16
|
const DAY = 24 * 3600 * 1000;
|
|
17
17
|
|
|
@@ -27,11 +27,12 @@ async function start() {
|
|
|
27
27
|
};
|
|
28
28
|
// 本地只保存“是否隐藏”和显示器;位置、大小、动效来自 Harness 设置(预览模式下才在本地保存)。
|
|
29
29
|
let settings=normalizeSettings(read('settings.json',{}));
|
|
30
|
-
let prefs={enabled:true,popups:true,details:true,balance:true,questions:true};
|
|
30
|
+
let prefs={enabled:true,popups:true,details:true,balance:true,questions:true,approvals:true,chat:true,hoverExpand:true};
|
|
31
31
|
let balance=null;
|
|
32
|
-
let answerResults={};
|
|
32
|
+
let answerResults={},approvalResults={},chatResults={};
|
|
33
|
+
let statsWin=null,statsRange='today';
|
|
33
34
|
let history=Array.isArray(read('history.json',[]))?read('history.json',[]):[];
|
|
34
|
-
let win, tray, ignore=true, hitbox=null, quitting=false, activity={state:'idle',activeCount:0}, demoTimer;
|
|
35
|
+
let win, tray, ignore=true, hitbox=null, quitting=false, menuOpen=false, activity={state:'idle',activeCount:0}, demoTimer;
|
|
35
36
|
const save=()=>write('settings.json',settings);
|
|
36
37
|
const visible=()=>settings.enabled&&prefs.enabled;
|
|
37
38
|
|
|
@@ -62,7 +63,8 @@ async function start() {
|
|
|
62
63
|
day:{hours,tokens:hours.reduce((a,b)=>a+b,0),cacheRate:input+cache>0?Math.round(cache/(input+cache)*100):null}
|
|
63
64
|
};
|
|
64
65
|
};
|
|
65
|
-
|
|
66
|
+
// Harness 侧的用量账本(含历史会话补算)优先;没有时退回本地的完成记录。
|
|
67
|
+
const payload=()=>({activity,settings,prefs,answerResults,approvalResults,chatResults,balance:prefs.balance?balance:null,preview:demo,history:activity.usage??summary(),displays:screen.getAllDisplays().map((d,i)=>({id:d.id,label:d.label||`显示器 ${i+1}`}))});
|
|
66
68
|
const send=()=>{ if(win && !win.isDestroyed())win.webContents.send('island:snapshot',payload()); };
|
|
67
69
|
const position=()=>{
|
|
68
70
|
const display=screen.getAllDisplays().find(d=>d.id===settings.displayId)||screen.getPrimaryDisplay();
|
|
@@ -76,11 +78,29 @@ async function start() {
|
|
|
76
78
|
const update=value=>{settings=normalizeSettings({...settings,...value});save();sync();};
|
|
77
79
|
const applyPrefs=value=>{
|
|
78
80
|
if(!value||typeof value!=='object')return;
|
|
79
|
-
prefs={enabled:value.enabled!==false,popups:value.popups!==false,details:value.details!==false,balance:value.balance!==false,questions:value.questions!==false};
|
|
81
|
+
prefs={enabled:value.enabled!==false,popups:value.popups!==false,details:value.details!==false,balance:value.balance!==false,questions:value.questions!==false,approvals:value.approvals!==false,chat:value.chat!==false,hoverExpand:value.hoverExpand!==false};
|
|
80
82
|
settings=normalizeSettings({...settings,position:value.position,scale:SIZES[value.size]??1,motion:value.reduceMotion?'reduced':'full'});
|
|
81
83
|
save();sync();
|
|
82
84
|
};
|
|
83
85
|
const openHarness=()=>shell.openExternal('dsh://open').catch(()=>{});
|
|
86
|
+
// 用量统计窗口:数据由 Harness 侧的用量账本按需计算(预览模式用示例数据)。
|
|
87
|
+
const requestStats=range=>{
|
|
88
|
+
statsRange=['today','7d','30d'].includes(range)?range:statsRange;
|
|
89
|
+
if(demo){const data=demoStats(statsRange);setTimeout(()=>statsWin?.webContents.send('stats:data',data),120);return;}
|
|
90
|
+
if(process.send)process.send({type:'stats',range:statsRange});
|
|
91
|
+
else statsWin?.webContents.send('stats:data',null);
|
|
92
|
+
};
|
|
93
|
+
const openStats=()=>{
|
|
94
|
+
if(statsWin&&!statsWin.isDestroyed()){statsWin.show();statsWin.focus();requestStats(statsRange);return;}
|
|
95
|
+
statsWin=new BrowserWindow({width:860,height:680,minWidth:620,minHeight:480,show:false,title:'用量统计 · DeepSeek 灵动岛',backgroundColor:'#0b0e17',autoHideMenuBar:true,
|
|
96
|
+
webPreferences:{preload:path.join(__dirname,'stats-preload.cjs'),contextIsolation:true,nodeIntegration:false,sandbox:true}});
|
|
97
|
+
statsWin.setMenuBarVisibility(false);
|
|
98
|
+
statsWin.webContents.setWindowOpenHandler(({url})=>{if(/^https:\/\/api-docs\.deepseek\.com\//.test(url))shell.openExternal(url).catch(()=>{});return {action:'deny'};});
|
|
99
|
+
statsWin.webContents.on('will-navigate',event=>event.preventDefault());
|
|
100
|
+
statsWin.once('ready-to-show',()=>statsWin.show());
|
|
101
|
+
statsWin.on('closed',()=>{statsWin=null;});
|
|
102
|
+
statsWin.loadFile(path.join(__dirname,'stats.html'),{query:{preview:demo?'1':''}});
|
|
103
|
+
};
|
|
84
104
|
const menu=()=>Menu.buildFromTemplate([
|
|
85
105
|
{label:visible()?'隐藏灵动岛':'显示灵动岛',click:()=>visible()?hide():show()},
|
|
86
106
|
{label:'显示器',submenu:screen.getAllDisplays().map((d,i)=>({label:d.label||`显示器 ${i+1}`,type:'radio',checked:(settings.displayId??screen.getPrimaryDisplay().id)===d.id,click:()=>update({displayId:d.id})}))},
|
|
@@ -91,6 +111,7 @@ async function start() {
|
|
|
91
111
|
{label:'播放全部状态',click:()=>playDemo()}
|
|
92
112
|
]:[{label:'位置与大小:Harness 设置 › 灵动岛',click:openHarness}]),
|
|
93
113
|
{type:'separator'},
|
|
114
|
+
{label:'用量统计',click:openStats},
|
|
94
115
|
{label:'打开 DeepSeek Harness',click:openHarness},
|
|
95
116
|
{label:'退出灵动岛',click:()=>app.quit()}
|
|
96
117
|
]);
|
|
@@ -107,7 +128,13 @@ async function start() {
|
|
|
107
128
|
if(!trusted(event)||!rect||!['x','y','width','height'].every(k=>Number.isFinite(rect[k])))return;
|
|
108
129
|
hitbox={x:Math.max(0,rect.x),y:Math.max(0,rect.y),width:Math.min(WIDTH,Math.max(0,rect.width)),height:Math.min(HEIGHT,Math.max(0,rect.height)),radius:Number.isFinite(rect.radius)?Math.max(0,rect.radius):0};
|
|
109
130
|
});
|
|
110
|
-
|
|
131
|
+
// 点击灵动岛以外的地方:窗口失去焦点,通知页面收起面板。弹出菜单期间不算,菜单关闭后再判断。
|
|
132
|
+
const dismiss=()=>{if(win&&!win.isDestroyed())win.webContents.send('island:blur');};
|
|
133
|
+
const blurred=()=>{if(!menuOpen)dismiss();};
|
|
134
|
+
const popupMenu=()=>{menuOpen=true;menu().popup({window:win,callback:()=>{menuOpen=false;setTimeout(()=>{if(!win.isDestroyed()&&!win.isFocused())dismiss();},60);}});};
|
|
135
|
+
ipcMain.on('island:menu',event=>{if(trusted(event))popupMenu();});
|
|
136
|
+
// 用户在灵动岛上按下鼠标时确保窗口拿到焦点,之后点别处才会触发失焦;悬停和自动弹出不抢焦点。
|
|
137
|
+
ipcMain.on('island:engage',event=>{if(trusted(event)&&win.isVisible()&&!win.isFocused())win.focus();});
|
|
111
138
|
ipcMain.on('island:open',event=>{if(trusted(event))openHarness();});
|
|
112
139
|
ipcMain.on('island:hide',event=>{if(trusted(event))hide();});
|
|
113
140
|
ipcMain.on('island:answer',(event,callId,answers)=>{
|
|
@@ -117,7 +144,23 @@ async function start() {
|
|
|
117
144
|
if(demo){answerResults={[callId]:{ok:true,at:Date.now()}};send();setTimeout(()=>setDemoState('thinking'),700);return;}
|
|
118
145
|
if(process.send)process.send({type:'answer',callId,answers:clean});
|
|
119
146
|
});
|
|
147
|
+
// 在灵动岛里发消息:只发给 Harness 告诉我们的那个对话,内容交给 Harness 的会话控制器处理。
|
|
148
|
+
ipcMain.on('island:chat',(event,id,sessionId,text,mode)=>{
|
|
149
|
+
if(!trusted(event)||typeof id!=='string'||id.length>80||typeof sessionId!=='string'||typeof text!=='string'||!text.trim()||text.length>8000)return;
|
|
150
|
+
if(!prefs.chat||activity.chat?.sessionId!==sessionId)return;
|
|
151
|
+
const how=mode==='steer'?'steer':'queue';
|
|
152
|
+
if(demo){chatResults={[id]:{ok:true,mode:how,at:Date.now()}};send();setTimeout(()=>setDemoState('thinking'),600);return;}
|
|
153
|
+
if(process.send)process.send({type:'chat',id,sessionId,text,mode:how});
|
|
154
|
+
});
|
|
120
155
|
ipcMain.on('island:panel',event=>{if(trusted(event)&&process.send)process.send({type:'panel-open'});});
|
|
156
|
+
ipcMain.on('island:stats',event=>{if(trusted(event))openStats();});
|
|
157
|
+
ipcMain.on('stats:request',(event,range)=>{if(statsWin&&event.sender===statsWin.webContents&&event.senderFrame===statsWin.webContents.mainFrame)requestStats(range);});
|
|
158
|
+
ipcMain.on('island:approve',(event,id,outcome)=>{
|
|
159
|
+
if(!trusted(event)||typeof id!=='string'||!['allowed-once','rejected'].includes(outcome))return;
|
|
160
|
+
if(!(activity.approvals||[]).some(a=>a.id===id))return;
|
|
161
|
+
if(demo){approvalResults={[id]:{ok:true,at:Date.now()}};activity={...activity,approvals:activity.approvals.filter(a=>a.id!==id)};send();return;}
|
|
162
|
+
if(process.send)process.send({type:'approve',id,outcome});
|
|
163
|
+
});
|
|
121
164
|
ipcMain.on('island:stop',(event,sessionId)=>{
|
|
122
165
|
if(!trusted(event)||typeof sessionId!=='string'||sessionId!==activity.sessionId||!activity.stoppable)return;
|
|
123
166
|
if(demo){setDemoState('paused');return;}
|
|
@@ -133,11 +176,13 @@ async function start() {
|
|
|
133
176
|
const busy=!done;
|
|
134
177
|
activity={state,title:'重构登录模块并补齐测试',sessionId:'preview',activeCount:busy?2:0,started:Date.now()-192000,ended:done?Date.now():0,
|
|
135
178
|
tool:state==='tools'?'str_replace_editor':'',stoppable:busy&&state!=='offline',
|
|
136
|
-
detail:state==='idle'?null:{plan:{total:7,done:state==='completed'?7:4,current:state==='completed'?'':'补齐登录失败场景的单元测试'},trail:['read','grep','str_replace_editor','bash'],toolCount:23,model:'deepseek-v4',contextTokens:
|
|
179
|
+
detail:state==='idle'?null:{plan:{total:7,done:state==='completed'?7:4,current:state==='completed'?'':'补齐登录失败场景的单元测试'},trail:['read','grep','str_replace_editor','bash'],toolCount:23,model:'deepseek-v4-pro',contextTokens:state==='compacting'?968000:412000,contextWindow:1048576,outputTokens:8210,cacheRate:94,...(state==='completed'?{reply:'登录模块已拆成 3 个文件,新增 14 个单元测试全部通过。验证码过期场景需要后端配合,我在 TODO 里留了说明。'}:{})},
|
|
137
180
|
sessions:busy?[{id:'preview',title:'重构登录模块并补齐测试',state,started:Date.now()-192000},{id:'p2',title:'整理周报数据',state:'streaming',started:Date.now()-41000}]:[],
|
|
138
181
|
questions:state==='waiting'?[{callId:'demo-ask',sessionId:'preview',state:'open',title:'重构登录模块并补齐测试',questions:[
|
|
139
182
|
{id:'framework',header:'测试框架',question:'登录模块的单元测试用哪个框架?',multiSelect:false,options:[{label:'Vitest',description:'和现有构建工具一致,速度快'},{label:'Jest',description:'生态成熟,迁移成本略高'},{label:'node:test',description:'零依赖'}]},
|
|
140
183
|
{id:'cases',header:'覆盖场景',question:'需要覆盖哪些失败场景?',multiSelect:true,options:[{label:'密码错误'},{label:'账号锁定'},{label:'验证码过期'}]}]}]:[],
|
|
184
|
+
approvals:state==='waiting'?[{id:'demo-approval',sessionId:'preview',title:'重构登录模块并补齐测试',at:Date.now(),toolName:'bash',reason:'运行测试需要执行命令',detail:{label:'命令',text:'npm test -- --coverage src/auth',mono:true}}]:[],
|
|
185
|
+
chat:{sessionId:'preview',title:'重构登录模块并补齐测试',running:busy,state,reply:busy?'':'登录模块已拆成 3 个文件,新增 14 个单元测试全部通过。\n\n验证码过期场景需要后端配合,我在 TODO 里留了说明。要我顺便把注册流程也按同样的方式拆一下吗?'},
|
|
141
186
|
finished:[]};
|
|
142
187
|
if(demo&&!history.length){
|
|
143
188
|
const sample=[['修复首页加载白屏','completed',22,312,18],['写一份竞品对比表','completed',17,146,9],['整理接口文档','completed',9,205,14],['升级依赖并跑测试','error',3,88,6],['生成周报图表','completed',1.5,131,11]];
|
|
@@ -146,12 +191,33 @@ async function start() {
|
|
|
146
191
|
}
|
|
147
192
|
send();
|
|
148
193
|
};
|
|
194
|
+
const demoStats=range=>{
|
|
195
|
+
const days=range==='30d'?30:range==='7d'?7:1,byHour=range==='today',start=new Date();start.setHours(0,0,0,0);start.setDate(start.getDate()-(days-1));
|
|
196
|
+
const from=start.getTime(),n=byHour?24:days,nowH=new Date().getHours();
|
|
197
|
+
const wave=i=>byHour?(i<=nowH&&i>=Math.min(9,Math.max(0,nowH-6))?0.4+Math.abs(Math.sin(i*1.7)):0):(0.3+Math.abs(Math.sin(i*2.1))*(i%7===5||i%7===6?0.3:1));
|
|
198
|
+
const buckets=Array.from({length:n},(_,i)=>{const w=wave(i),d=new Date(from);if(byHour)d.setHours(i);else d.setDate(d.getDate()+i);
|
|
199
|
+
const cacheRead=Math.round(w*(byHour?380000:2.6e6)),input=Math.round(w*(byHour?21000:150000)),output=Math.round(w*(byHour?9000:64000));
|
|
200
|
+
return {start:d.getTime(),label:byHour?`${i}:00`:`${d.getMonth()+1}/${d.getDate()}`,cacheRead,input,output,cost:(cacheRead*0.02+input*1+output*4)/1e6*(i%3?1:2.2),turns:w?Math.max(1,Math.round(w*(byHour?3:14))):0};});
|
|
201
|
+
const sum=k=>buckets.reduce((a,b)=>a+b[k],0),tokens=sum('cacheRead')+sum('input')+sum('output');
|
|
202
|
+
const titles=['重构登录模块并补齐测试','整理周报数据','修复首页加载白屏','写一份竞品对比表','整理接口文档','升级依赖并跑测试'];
|
|
203
|
+
const sessions=titles.slice(0,byHour?4:6).map((title,i)=>{const share=[.38,.22,.16,.11,.08,.05][i],turns=Math.max(1,Math.round(sum('turns')*share)),last=Date.now()-i*(byHour?3600e3:86400e3*days/7);
|
|
204
|
+
const items=Array.from({length:Math.min(turns,8)},(_,k)=>{const end=last-k*1500e3,inp=9000+k*700,cr=120000+k*9000,out=3800+k*300,model=i===0&&k<2?'deepseek-v4-pro':'deepseek-flash';
|
|
205
|
+
return {turn:turns-k,start:end-(120+k*40)*1000,end,state:k===2&&i===3?'error':'completed',model,calls:3+k%4,tools:5+k%6,input:inp,cacheRead:cr,output:out,cost:model==='deepseek-flash'?(inp+cr*0.02+out*4)/1e6:(inp*4.5+cr*0.15+out*13.5)/1e6,priced:true};});
|
|
206
|
+
return {id:'demo'+i,title,tokens:Math.round(tokens*share),cost:sum('cost')*share,turns,last,items};});
|
|
207
|
+
return {range,from,to:Date.now(),byHour,buckets,preview:true,
|
|
208
|
+
totals:{tokens,input:sum('input'),cacheRead:sum('cacheRead'),output:sum('output'),cost:sum('cost'),unpricedTokens:0,turns:sum('turns'),completed:Math.round(sum('turns')*.93),ms:sum('turns')*184000,cacheRate:tokens?Math.round(sum('cacheRead')/(sum('cacheRead')+sum('input'))*100):null},
|
|
209
|
+
models:[{model:'deepseek-flash',priced:true,tokens:Math.round(tokens*.78),cost:sum('cost')*.46,turns:Math.round(sum('turns')*.8)},{model:'deepseek-v4-pro',priced:true,tokens:Math.round(tokens*.22),cost:sum('cost')*.54,turns:Math.round(sum('turns')*.2)}],
|
|
210
|
+
sessions,scanned:42};
|
|
211
|
+
};
|
|
149
212
|
const playDemo=()=>{clearInterval(demoTimer);let i=0;const order=['idle','thinking','streaming','tools','waiting','completed','paused','error','reconnecting','compacting','offline'];setDemoState(order[i++]);demoTimer=setInterval(()=>setDemoState(order[i++%order.length]),4500);};
|
|
150
213
|
process.on('message',message=>{
|
|
151
214
|
if(message?.type==='shutdown')app.quit();
|
|
152
215
|
if(message?.type==='prefs')applyPrefs(message.data);
|
|
153
216
|
if(message?.type==='show')show();
|
|
154
217
|
if(message?.type==='answer-result'&&typeof message.callId==='string'){answerResults={...Object.fromEntries(Object.entries(answerResults).filter(([,r])=>Date.now()-r.at<300000)),[message.callId]:{ok:!!message.ok,reason:String(message.reason||'').slice(0,80),at:Date.now()}};send();}
|
|
218
|
+
if(message?.type==='chat-result'&&typeof message.id==='string'){chatResults={...Object.fromEntries(Object.entries(chatResults).filter(([,r])=>Date.now()-r.at<300000)),[message.id]:{ok:!!message.ok,mode:message.mode,reason:String(message.reason||'').slice(0,80),at:Date.now()}};send();}
|
|
219
|
+
if(message?.type==='approval-result'&&typeof message.id==='string'){approvalResults={...Object.fromEntries(Object.entries(approvalResults).filter(([,r])=>Date.now()-r.at<300000)),[message.id]:{ok:!!message.ok,reason:String(message.reason||'').slice(0,80),at:Date.now()}};send();}
|
|
220
|
+
if(message?.type==='stats'&&statsWin&&!statsWin.isDestroyed())statsWin.webContents.send('stats:data',message.data??null);
|
|
155
221
|
if(message?.type==='balance'){balance=message.data&&Array.isArray(message.data.wallets)?message.data:null;send();}
|
|
156
222
|
if(message?.type==='activity'&&Object.hasOwn(STATES,message.data?.state)){activity=message.data;remember(activity.finished);send();}
|
|
157
223
|
});
|
|
@@ -174,8 +240,10 @@ async function start() {
|
|
|
174
240
|
},40);
|
|
175
241
|
screen.on('display-metrics-changed',position); screen.on('display-removed',position);screen.on('display-added',position);
|
|
176
242
|
app.on('second-instance',show);
|
|
243
|
+
win.on('blur',blurred);
|
|
177
244
|
win.on('close',event=>{if(!quitting){event.preventDefault();hide();}});
|
|
178
|
-
app.on('before-quit',()=>{quitting=true;clearInterval(cursorTimer);clearInterval(demoTimer);tray?.destroy();});
|
|
245
|
+
app.on('before-quit',()=>{quitting=true;statsWin?.destroy();clearInterval(cursorTimer);clearInterval(demoTimer);tray?.destroy();});
|
|
179
246
|
if(process.send) process.send({type:'ready'});
|
|
180
247
|
if(demo && process.argv.includes('--cycle'))playDemo();
|
|
248
|
+
if(process.argv.includes('--stats'))openStats();
|
|
181
249
|
}
|
package/desktop/preload.cjs
CHANGED
|
@@ -2,6 +2,8 @@ const {contextBridge,ipcRenderer}=require('electron');
|
|
|
2
2
|
contextBridge.exposeInMainWorld('island',{
|
|
3
3
|
initial:()=>ipcRenderer.invoke('island:initial'),
|
|
4
4
|
subscribe:callback=>{const fn=(_event,data)=>callback(data);ipcRenderer.on('island:snapshot',fn);return()=>ipcRenderer.removeListener('island:snapshot',fn);},
|
|
5
|
+
onBlur:callback=>{const fn=()=>callback();ipcRenderer.on('island:blur',fn);return()=>ipcRenderer.removeListener('island:blur',fn);},
|
|
6
|
+
engage:()=>ipcRenderer.send('island:engage'),
|
|
5
7
|
settings:value=>ipcRenderer.send('island:settings',value),
|
|
6
8
|
bounds:value=>ipcRenderer.send('island:bounds',value),
|
|
7
9
|
menu:()=>ipcRenderer.send('island:menu'),
|
|
@@ -10,5 +12,8 @@ contextBridge.exposeInMainWorld('island',{
|
|
|
10
12
|
stop:sessionId=>ipcRenderer.send('island:stop',sessionId),
|
|
11
13
|
panel:()=>ipcRenderer.send('island:panel'),
|
|
12
14
|
answer:(callId,answers)=>ipcRenderer.send('island:answer',callId,answers),
|
|
15
|
+
approve:(id,outcome)=>ipcRenderer.send('island:approve',id,outcome),
|
|
16
|
+
chat:(id,sessionId,text,mode)=>ipcRenderer.send('island:chat',id,sessionId,text,mode),
|
|
17
|
+
stats:()=>ipcRenderer.send('island:stats'),
|
|
13
18
|
preview:state=>ipcRenderer.send('island:preview',state)
|
|
14
19
|
});
|