@pointylab/dsh 0.0.0-stage → 0.4.3

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.
Files changed (88) hide show
  1. package/ASSETS.md +20 -0
  2. package/LICENSE +21 -0
  3. package/README.md +85 -2
  4. package/cordis.patch.yml +13 -0
  5. package/lib/client.js +33 -0
  6. package/package.json +38 -4
  7. package/pet/app/card/card.css +365 -0
  8. package/pet/app/card/card.html +68 -0
  9. package/pet/app/card/card.js +896 -0
  10. package/pet/app/card/markdown.js +64 -0
  11. package/pet/app/card/mascot/blink.png +0 -0
  12. package/pet/app/card/mascot/done.png +0 -0
  13. package/pet/app/card/mascot/error.png +0 -0
  14. package/pet/app/card/mascot/idle.png +0 -0
  15. package/pet/app/card/mascot/peek.png +0 -0
  16. package/pet/app/card/mascot/think.png +0 -0
  17. package/pet/app/card/preload.js +21 -0
  18. package/pet/app/fullscreen.js +131 -0
  19. package/pet/app/main.js +1252 -0
  20. package/pet/app/native/uia-selection.ps1 +158 -0
  21. package/pet/app/notice/notice.html +37 -0
  22. package/pet/app/notice/notice.js +31 -0
  23. package/pet/app/notice/preload.js +7 -0
  24. package/pet/app/overlay/overlay.html +30 -0
  25. package/pet/app/overlay/overlay.js +42 -0
  26. package/pet/app/overlay/preload.js +7 -0
  27. package/pet/app/pill/pill.css +73 -0
  28. package/pet/app/pill/pill.html +11 -0
  29. package/pet/app/pill/pill.js +35 -0
  30. package/pet/app/pill/preload.js +8 -0
  31. package/pet/app/preload.js +16 -0
  32. package/pet/app/renderer/index.html +24 -0
  33. package/pet/app/renderer/lines.js +25 -0
  34. package/pet/app/renderer/pet.js +292 -0
  35. package/pet/app/renderer/renderer2d.js +71 -0
  36. package/pet/app/renderer/style.css +157 -0
  37. package/pet/app/selection.js +246 -0
  38. package/pet/app/shake.js +76 -0
  39. package/pet/app/uipick.js +314 -0
  40. package/pet/assets/cursor/cursor-32.cur +0 -0
  41. package/pet/assets/cursor/cursor-32.png +0 -0
  42. package/pet/assets/cursor/cursor-48.cur +0 -0
  43. package/pet/assets/cursor/cursor-48.png +0 -0
  44. package/pet/assets/cursor/cursor-64.cur +0 -0
  45. package/pet/assets/cursor/cursor-64.png +0 -0
  46. package/pet/assets/cursor/cursor-clean.png +0 -0
  47. package/pet/assets/cursor/make_cursor.py +85 -0
  48. package/pet/assets/cursor/source.png +0 -0
  49. package/pet/assets/pointy.ico +0 -0
  50. package/pet/assets/tray.png +0 -0
  51. package/pet/assets/tray@2x.png +0 -0
  52. package/pet/boot.js +6 -0
  53. package/pet/extension/background.js +252 -0
  54. package/pet/extension/cursor-32.png +0 -0
  55. package/pet/extension/cursor-48.png +0 -0
  56. package/pet/extension/cursor-64.png +0 -0
  57. package/pet/extension/icon.png +0 -0
  58. package/pet/extension/manifest.json +12 -0
  59. package/pet/extension/pick.js +391 -0
  60. package/pet/extension/popup.html +29 -0
  61. package/pet/extension/popup.js +30 -0
  62. package/pet/package.json +11 -0
  63. package/pet/vendor/koffi/LICENSE.txt +22 -0
  64. package/pet/vendor/koffi/build/koffi/win32_x64/koffi.exp +0 -0
  65. package/pet/vendor/koffi/build/koffi/win32_x64/koffi.lib +0 -0
  66. package/pet/vendor/koffi/build/koffi/win32_x64/koffi.node +0 -0
  67. package/pet/vendor/koffi/index.js +634 -0
  68. package/pet/vendor/koffi/indirect.js +533 -0
  69. package/pet/vendor/koffi/package.json +35 -0
  70. package/pet/vendor/ws/LICENSE +20 -0
  71. package/pet/vendor/ws/index.js +22 -0
  72. package/pet/vendor/ws/lib/buffer-util.js +142 -0
  73. package/pet/vendor/ws/lib/constants.js +19 -0
  74. package/pet/vendor/ws/lib/event-target.js +292 -0
  75. package/pet/vendor/ws/lib/extension.js +203 -0
  76. package/pet/vendor/ws/lib/limiter.js +55 -0
  77. package/pet/vendor/ws/lib/permessage-deflate.js +530 -0
  78. package/pet/vendor/ws/lib/receiver.js +743 -0
  79. package/pet/vendor/ws/lib/sender.js +610 -0
  80. package/pet/vendor/ws/lib/stream.js +161 -0
  81. package/pet/vendor/ws/lib/subprotocol.js +62 -0
  82. package/pet/vendor/ws/lib/validation.js +152 -0
  83. package/pet/vendor/ws/lib/websocket-server.js +562 -0
  84. package/pet/vendor/ws/lib/websocket.js +1426 -0
  85. package/pet/vendor/ws/package.json +74 -0
  86. package/pet/vendor/ws/wrapper.mjs +21 -0
  87. package/src/node.js +732 -0
  88. package/src/runtime.js +178 -0
package/ASSETS.md ADDED
@@ -0,0 +1,20 @@
1
+ # 图片素材
2
+
3
+ Pointy 的代码以 MIT 发布(见 [LICENSE](LICENSE)),但下面这些美术素材**不在 MIT 授权范围内**。
4
+
5
+ 鲸鱼娘形象是 DeepSeek 鲸鱼娘的同人创作:原作 上善无形,像素重绘 ZipZipPipe,按 [CC BY-NC-SA 4.0](https://creativecommons.org/licenses/by-nc-sa/4.0/deed.zh-hans) 授权。使用时要署名,不能用于商业用途,改编后的作品要用相同协议发布。
6
+
7
+ 你可以为了构建、测试和运行本仓库而使用它们。如果以修改版或衍生作品的身份再发布,请遵守上面的授权条件,或者换成自己的素材。
8
+
9
+ | 素材 | 位置 | 来源 |
10
+ |---|---|---|
11
+ | 卡片旁边的像素鲸鱼娘 | `app/card/mascot/` | 鲸鱼娘同人(上善无形 原作,ZipZipPipe 像素重绘) |
12
+ | 指认时的鼠标光标 | `assets/cursor/`、`extension/cursor-*.png` | 由上面的鲸鱼娘形象改成光标 |
13
+ | 托盘图标、Pointy.exe 的图标 | `assets/tray.png`、`assets/tray@2x.png`(打包时拼成 `pointy.ico`) | 同上 |
14
+ | 浏览器扩展图标 | `extension/icon.png` | 由 `assets/tray@2x.png` 放大 |
15
+
16
+ 这些鲸鱼娘形象是非官方的同人形象,和 DeepSeek 没有关系,也不代表 DeepSeek。如果权利人提出要求,我们会替换。
17
+
18
+ 光标由 `assets/cursor/make_cursor.py` 从 `assets/cursor/source.png` 生成,脚本本身和其他源码一样以 MIT 发布。
19
+
20
+ 第三方代码:随插件包分发的 [koffi](https://koffi.dev)(MIT)和 [ws](https://github.com/websockets/ws)(MIT)保留各自的许可证,在 `pet/vendor/` 下;插件下载的 [Electron](https://www.electronjs.org)(MIT)是官方原版,只改了程序名、描述和图标。
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 pointylab
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,86 @@
1
- # Temporary Holding Version
1
+ # Pointy
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ DeepSeek Harness(DSH)插件。在电脑上**晃一下鼠标**,指到网页、文档、聊天窗口里的任意内容,点一下就能问 DSH,回答在原地的小卡片里流出来。只支持 Windows 10/11 x64。
4
+
5
+ ## 安装
6
+
7
+ **图形界面**:DSH 里打开「插件管理」→ 安装第三方插件,在安装框里填包名:
8
+
9
+ ```
10
+ @pointylab/dsh
11
+ ```
12
+
13
+ **命令行**:
14
+
15
+ ```bash
16
+ dsh plugin --profile desktop add @pointylab/dsh
17
+ ```
18
+
19
+ 用 web 版 DSH 的把 `desktop` 换成 `web`。
20
+
21
+ 装完**重新打开 DSH**。第一次启用时插件会下载 Pointy 用的运行时(Electron,下载约 150 MB、解压后约 370 MB,国内走 npmmirror 镜像,按官方 SHA-256 校验),放在 `%USERPROFILE%\.pointy\runtime`,只下一次。下完托盘里出现鲸鱼图标,右下角弹出使用说明。之后 Pointy 随 DSH 启动、随 DSH 退出。
22
+
23
+ ### 浏览器扩展(推荐)
24
+
25
+ 没装扩展也能在浏览器里用,网页按截图处理。装了扩展能指认到网页里具体的段落、图片,还多一个右键「问 Pointy」。
26
+
27
+ 1. 托盘鲸鱼图标 → 「安装浏览器扩展(推荐)…」,会打开扩展所在的文件夹 `%USERPROFILE%\.pointy\extension`。
28
+ 2. Chrome 打开 `chrome://extensions`(Edge 是 `edge://extensions`),打开右上角「开发者模式」,点「加载已解压的扩展程序」,选这个文件夹。
29
+ 3. 扩展会自己连上 Pointy,屏幕右下角弹窗问是否允许,点「允许」。
30
+
31
+ ## 用法
32
+
33
+ - 晃一下鼠标进入指认,指到的内容会框出来,点一下弹出卡片;Esc 取消。
34
+ - 程序里读不到文字的地方(QQ、网易云这类整窗都是一张图的),指认时是一个虚线小框,滚轮调大小,截框里的图发过去。
35
+ - 卡片上的 × 或 Esc:问过问题的卡片缩成右下角的小胶囊,点胶囊回到卡片;↗ 跳到 DSH 里对应的会话。
36
+ - 托盘菜单:晃动灵敏度、勿扰模式、浏览器扩展、退出。
37
+ - 以管理员身份运行的程序读不到,会提示。
38
+
39
+ ## 卸载
40
+
41
+ DSH「插件管理」里删掉 @pointylab/dsh,或者:
42
+
43
+ ```bash
44
+ dsh plugin --profile desktop remove @pointylab/dsh
45
+ ```
46
+
47
+ 再删掉 `%USERPROFILE%\.pointy` 文件夹(运行时、设置、和扩展的配对信息都在里面)。
48
+
49
+ ---
50
+
51
+ ## 开发
52
+
53
+ - `app\`:Pointy 本体(Electron)。托盘、晃动检测、指认、卡片、胶囊;在 `127.0.0.1:47321` 开 WebSocket,DSH 插件和浏览器扩展都连这里。
54
+ - `plugin\`:DSH 插件 `@pointylab/dsh`。宿主那一半(`src\node.js`)把会话状态、回答推给 Pointy,必要时下载运行时并拉起 Pointy(`src\runtime.js`);网页那一半(`lib\client.js`)负责切会话。
55
+ - `extension\`:Chrome / Edge 扩展。
56
+
57
+ DSH 的插件宿主是 DSH 自己的程序以 node 模式跑的子进程,开不了窗口,所以 Pointy 仍是单独的 Electron 进程,只是由插件带着、由插件拉起。
58
+
59
+ ### 直接跑开发版
60
+
61
+ ```bash
62
+ npm install
63
+ npm start
64
+ ```
65
+
66
+ 开发版和插件拉起的那份共用 `%USERPROFILE%\.pointy` 和单实例锁:开发版开着时,插件发现 47321 端口已有 Pointy,就直接连它,不再拉起自带的。
67
+
68
+ ### 打包插件
69
+
70
+ ```bash
71
+ node tools/pack-plugin.mjs
72
+ ```
73
+
74
+ 把 `app`、`assets`、`extension` 抄进 `plugin\pet`,koffi(只留 Windows x64 的预编译文件)和 ws 放进 `plugin\pet\vendor`,钉住 Electron 版本和官方 SHA-256,`npm pack` 出 `dist\pointylab-dsh-<版本>.tgz`。koffi 不作为 npm 依赖:pnpm 默认拦依赖的安装脚本,koffi 有一个,拦了 `dsh plugin add` 会报错。插件唯一的依赖是 resedit(纯 JS),下载完 Electron 后用它把 electron.exe 改成 Pointy.exe、换描述和图标。
75
+
76
+ 本地试装:`dsh plugin --profile <profile> add file:<tgz 的绝对路径>`。每次改完先把 `plugin\package.json` 的版本号加一,同名同版本 pnpm 可能用缓存;不要用 `link:`(`remove` 会连源码目录一起删)。
77
+
78
+ ### 测试用的环境变量
79
+
80
+ - `POINTY_PORT`:改端口,Pointy 和插件两边都认。
81
+ - `POINTY_TEST_HIDDEN=1`:不显示任何窗口、不装托盘,配合 `--inspect` 和 `global.pointyTest` 测试。插件拉起 Pointy 时这两个变量会传下去。
82
+ - `npm run fake`:不开 DSH,冒充插件推一遍状态。
83
+
84
+ ## 许可
85
+
86
+ 代码是 MIT([LICENSE](LICENSE))。鲸鱼娘的图片是同人作品,按 CC BY-NC-SA 4.0 授权,不能商用,详见 [ASSETS.md](ASSETS.md)。
@@ -0,0 +1,13 @@
1
+ # @pointylab/dsh bundle layer. The node half talks to the pet over a loopback
2
+ # WebSocket; the browser half (lib/client.js, found through package.json's
3
+ # dsh.client) asks the node half which session to open over a private RPC
4
+ # channel, which needs Connection to hold the web server.
5
+ - id: connection
6
+ name: '@deepseek-ai/dsh-client-connection'
7
+ inject: [webStartup, webServer]
8
+ config:
9
+ trustedHosts: !!js ctx.webStartup.trustedHosts
10
+
11
+ - insert:
12
+ - id: pointy
13
+ name: '@pointylab/dsh'
package/lib/client.js ADDED
@@ -0,0 +1,33 @@
1
+ // @pointylab/dsh 的界面那一半:在 DSH 页面里长轮询宿主,Pointy卡片点了"在 DSH 中打开"时,
2
+ // 宿主回一个会话 id,这里用 DSH 自己的 uiWorkspace.openSession 切过去。手写的,不经过打包。
3
+ window.__ModuleLoader__.load({
4
+ id: "@pointylab/dsh",
5
+ factory: () => {
6
+ var module = { exports: {} };
7
+ var exports = module.exports;
8
+ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
9
+ const RPC_CHANNEL = "/pointy";
10
+ const RETRY_MS = 3000;
11
+ const inject = ["connection", "uiWorkspace"];
12
+ function apply(ctx) {
13
+ const connection = ctx.get("connection");
14
+ let alive = true;
15
+ ctx.effect(() => () => { alive = false; }, "pointy: focus loop");
16
+ (async () => {
17
+ while (alive) {
18
+ try {
19
+ // 宿主最多挂 20 秒,没有要打开的会话就回 null,再问一次
20
+ const result = await connection.rpc.call(RPC_CHANNEL, "next-focus", { origin: location.origin });
21
+ if (!alive) break;
22
+ if (result?.ok && typeof result.value === "string") ctx.uiWorkspace.openSession(result.value);
23
+ } catch {
24
+ await new Promise((r) => setTimeout(r, RETRY_MS));
25
+ }
26
+ }
27
+ })();
28
+ }
29
+ exports.apply = apply;
30
+ exports.inject = inject;
31
+ return module.exports;
32
+ }
33
+ });
package/package.json CHANGED
@@ -1,6 +1,40 @@
1
1
  {
2
2
  "name": "@pointylab/dsh",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "0.4.3",
4
+ "description": "Pointy for DeepSeek Harness: shake the mouse, point at anything on screen and ask DSH about it",
5
+ "license": "MIT",
6
+ "keywords": ["dsh-plugin", "deepseek-harness", "pointy"],
7
+ "homepage": "https://github.com/pointylab/pointy#readme",
8
+ "repository": { "type": "git", "url": "git+https://github.com/pointylab/pointy.git", "directory": "plugin" },
9
+ "bugs": "https://github.com/pointylab/pointy/issues",
10
+ "type": "module",
11
+ "main": "src/node.js",
12
+ "exports": {
13
+ ".": "./src/node.js",
14
+ "./client": "./lib/client.js",
15
+ "./package.json": "./package.json"
16
+ },
17
+ "files": [
18
+ "src",
19
+ "lib",
20
+ "pet",
21
+ "cordis.patch.yml",
22
+ "ASSETS.md"
23
+ ],
24
+ "os": ["win32"],
25
+ "dependencies": {
26
+ "resedit": "^3.1.0"
27
+ },
28
+ "dsh": {
29
+ "bundle": {
30
+ "patch": "./cordis.patch.yml"
31
+ },
32
+ "client": {
33
+ "platform": "web",
34
+ "inject": [
35
+ "@deepseek-ai/dsh-client-connection",
36
+ "@deepseek-ai/dsh-client-ui-workspace"
37
+ ]
38
+ }
39
+ }
40
+ }
@@ -0,0 +1,365 @@
1
+ [hidden] { display: none !important; }
2
+
3
+ :root {
4
+ --bg: #ffffff;
5
+ --text: #1f2328;
6
+ --muted: #6e7781;
7
+ --faint: #9aa1aa;
8
+ --line: #e6e8eb;
9
+ --quote: #d0d7de;
10
+ --chip: #f2f3f5;
11
+ --chip-hover: #e8eaed;
12
+ --accent: #1f2328;
13
+ --accent-text: #ffffff;
14
+ --code: #f4f5f7;
15
+ --you: #f2f3f5;
16
+ --error: #cf222e;
17
+ --blue: #4d6bfe;
18
+ --blue-soft: #eef2ff;
19
+ --blue-line: #c9d4ff;
20
+ /* 窗口四周只留 18px,阴影不能扩散得比这更远,否则在窗口边上被切成一条硬边 */
21
+ --shadow: 0 3px 12px rgba(15, 23, 42, 0.08), 0 1px 3px rgba(15, 23, 42, 0.05), 0 0 0 1px rgba(15, 23, 42, 0.05);
22
+ /* 看板娘:窗口左边多留出她的位置,卡片本身的位置不变(main.js 里 MASCOT_ROOM 要和这里一致) */
23
+ --card-left: 68px;
24
+ --mascot-w: 70px;
25
+ --mascot-h: 134px;
26
+ --mascot-top: 30px;
27
+ /* 图(70×134 像素格)第 66 列起压到卡片上:竖线贴着卡片左边,手指和水滴搭在卡片上 */
28
+ --mascot-over: 4px;
29
+ }
30
+ @media (prefers-color-scheme: dark) {
31
+ :root {
32
+ --bg: #23252a;
33
+ --text: #e6e8eb;
34
+ --muted: #a0a7b1;
35
+ --faint: #7c838d;
36
+ --line: #34373d;
37
+ --quote: #4a4f57;
38
+ --chip: #2f3237;
39
+ --chip-hover: #3a3d43;
40
+ --accent: #e6e8eb;
41
+ --accent-text: #1f2328;
42
+ --code: #2c2f34;
43
+ --you: #2f3237;
44
+ --error: #ff7b72;
45
+ --blue: #6b84ff;
46
+ --blue-soft: #2a3150;
47
+ --blue-line: #3f4c86;
48
+ --shadow: 0 3px 12px rgba(0, 0, 0, 0.35), 0 1px 3px rgba(0, 0, 0, 0.2), 0 0 0 1px rgba(255, 255, 255, 0.06);
49
+ }
50
+ }
51
+
52
+ html, body {
53
+ margin: 0;
54
+ height: 100%;
55
+ background: transparent;
56
+ overflow: hidden;
57
+ font-family: "Segoe UI Variable Text", "Segoe UI", "Microsoft YaHei UI", "PingFang SC", sans-serif;
58
+ font-size: 14.5px;
59
+ color: var(--text);
60
+ }
61
+
62
+ /* 窗口四周留出阴影的位置 */
63
+ #card {
64
+ position: absolute;
65
+ inset: 18px 18px 18px var(--card-left);
66
+ z-index: 1;
67
+ display: flex;
68
+ flex-direction: column;
69
+ padding: 14px 18px 14px;
70
+ border-radius: 16px;
71
+ background: var(--bg);
72
+ box-shadow: var(--shadow);
73
+ overflow: hidden;
74
+ animation: pop 150ms ease-out;
75
+ }
76
+ ::selection { background: var(--blue-line); }
77
+
78
+ /* ---------- 看板娘 ---------- */
79
+ #mascot {
80
+ position: absolute;
81
+ left: calc(var(--card-left) + var(--mascot-over) - var(--mascot-w));
82
+ top: var(--mascot-top);
83
+ width: var(--mascot-w);
84
+ height: var(--mascot-h);
85
+ z-index: 2;
86
+ -webkit-app-region: drag;
87
+ animation: peek 420ms 140ms cubic-bezier(0.2, 0.9, 0.3, 1.25) both;
88
+ }
89
+ #mascot img {
90
+ display: block;
91
+ width: 100%; height: 100%;
92
+ object-fit: contain;
93
+ object-position: right top;
94
+ filter: drop-shadow(0 1px 1.5px rgba(15, 23, 42, 0.18));
95
+ pointer-events: none;
96
+ }
97
+ /* 呼吸:一格一格地上下 1px,像素风不要平滑 */
98
+ #mascot.idle img, #mascot.think img { animation: breathe 2.4s steps(1) infinite; }
99
+ #mascot.done img { animation: hop 520ms cubic-bezier(0.3, 1.6, 0.5, 1) 1; }
100
+ #mascot.error img { transform: translateY(3px); transition: transform 300ms ease; }
101
+ @keyframes peek { from { opacity: 0; transform: translateX(26px); } to { opacity: 1; transform: none; } }
102
+ @keyframes breathe { 0%, 100% { transform: none; } 50% { transform: translateY(1px); } }
103
+ @keyframes hop { 0%, 100% { transform: none; } 40% { transform: translateY(-5px); } }
104
+ #mascot:hover img { animation: wiggle 360ms steps(4) 1; }
105
+ @keyframes wiggle { 25% { transform: translateX(-1px); } 50% { transform: translateX(1px); } 75% { transform: translateX(-1px); } }
106
+
107
+ @keyframes pop { from { opacity: 0; transform: scale(0.96); } to { opacity: 1; transform: none; } }
108
+ /* 从右下角胶囊放回来:从右下往回放大 */
109
+ @keyframes restore { from { opacity: 0; transform: scale(0.3); } to { opacity: 1; transform: none; } }
110
+ /* 收进右下角胶囊:往右下角缩小淡出,窗口同时滑向托盘 */
111
+ @keyframes shrink { to { opacity: 0; transform: scale(0.3); } }
112
+ #card.shrinking { animation: shrink 220ms cubic-bezier(.4,0,.8,.6) forwards; }
113
+ #card.shrinking, #card.restoring { transform-origin: 100% 100%; }
114
+
115
+ button { font: inherit; color: inherit; }
116
+ /* 键盘聚焦(Tab 切换时)用淡蓝描边,不用系统默认的橙色 */
117
+ button:focus-visible { outline: 2px solid var(--blue-line); outline-offset: 1px; }
118
+ /* 只有对话区可以被压扁(滚动),其他部分量出来的高度才是真高度 */
119
+ #card > * { flex: none; }
120
+ #card > #thread { flex: 1 1 auto; }
121
+
122
+ /* ---------- 顶栏 ---------- */
123
+ header { display: flex; align-items: center; gap: 8px; -webkit-app-region: drag; }
124
+ .spacer { flex: 1; }
125
+ /* 状态圈不显示了(看板娘表情和收起后的胶囊已经说明状态),只留着给脚本记状态 */
126
+ #status { display: none; }
127
+
128
+ #target-wrap { -webkit-app-region: no-drag; position: relative; min-width: 0; }
129
+ #target {
130
+ display: inline-flex; align-items: center; gap: 4px;
131
+ max-width: 270px;
132
+ padding: 2px 6px 2px 4px;
133
+ border: none;
134
+ border-radius: 6px;
135
+ background: transparent;
136
+ color: var(--muted);
137
+ font-size: 13.5px;
138
+ cursor: pointer;
139
+ }
140
+ #target-label { min-width: 0; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
141
+ #target-meta { flex: none; white-space: nowrap; color: var(--faint); }
142
+ #target-meta.running { color: #2da44e; }
143
+ #target svg { flex: none; width: 10px; height: 10px; fill: none; stroke: currentColor; stroke-width: 1.3; }
144
+ #target:hover { background: var(--chip); }
145
+ #target:disabled { cursor: default; background: transparent; }
146
+ #target:disabled svg { display: none; }
147
+
148
+ /* 发送目标下拉:最近 5 个会话 + 新任务 */
149
+ #target-menu {
150
+ position: fixed; z-index: 10;
151
+ width: 300px; padding: 6px;
152
+ border-radius: 12px;
153
+ background: var(--bg);
154
+ box-shadow: var(--shadow);
155
+ border: 1px solid var(--line);
156
+ }
157
+ .opt {
158
+ display: flex; align-items: center; gap: 8px; width: 100%;
159
+ padding: 7px 8px; border: none; border-radius: 8px; background: none;
160
+ text-align: left; cursor: pointer; font-size: 13.5px;
161
+ }
162
+ .opt:hover, .opt.focus { background: var(--chip); }
163
+ .opt .tick { flex: none; width: 14px; color: var(--text); }
164
+ .opt .name { flex: 1; min-width: 0; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
165
+ .opt .meta { flex: none; color: var(--faint); font-size: 12px; }
166
+ .opt .meta.running { color: #2da44e; }
167
+ .opt .body { flex: 1; min-width: 0; display: flex; flex-direction: column; }
168
+ .opt .body .name { flex: none; }
169
+ .opt .sub { color: var(--faint); font-size: 12px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
170
+ .opt .sub:empty { display: none; }
171
+ .sep { height: 1px; margin: 4px 6px; background: var(--line); }
172
+ .menu-hint { padding: 4px 8px 2px; color: var(--faint); font-size: 11.5px; }
173
+ .menu-search {
174
+ display: block; width: 100%; margin-bottom: 4px; padding: 6px 9px;
175
+ border: 1px solid var(--line); border-radius: 8px; outline: none;
176
+ background: var(--bg); color: var(--text); font: inherit; font-size: 13px;
177
+ }
178
+ .menu-search:focus { border-color: var(--blue-line); }
179
+ .menu-search::placeholder { color: var(--faint); }
180
+ .menu-list { max-height: 264px; overflow-y: auto; }
181
+ .menu-note { padding: 6px 8px; color: var(--faint); font-size: 12px; }
182
+ /* 新任务那行右边的工作区 */
183
+ .opt .ws {
184
+ flex: none; display: flex; align-items: center; gap: 3px; max-width: 150px;
185
+ padding: 2px 6px; margin: -2px -4px -2px 0; border-radius: 6px;
186
+ color: var(--muted); font-size: 12px; white-space: nowrap;
187
+ }
188
+ .opt .ws span { overflow: hidden; text-overflow: ellipsis; }
189
+ .opt .ws svg { flex: none; width: 9px; height: 9px; fill: none; stroke: currentColor; stroke-width: 1.3; }
190
+ .opt .ws:hover { background: var(--chip-hover); color: var(--text); }
191
+ .ws-list { margin-left: 22px; }
192
+
193
+ .icon {
194
+ -webkit-app-region: no-drag;
195
+ width: 26px; height: 26px;
196
+ display: inline-flex; align-items: center; justify-content: center;
197
+ border: none; border-radius: 6px; background: transparent; cursor: pointer; color: var(--muted);
198
+ }
199
+ .icon:hover { background: var(--chip); color: var(--text); }
200
+ .icon svg { width: 15px; height: 15px; fill: none; stroke: currentColor; stroke-width: 1.4; stroke-linecap: round; stroke-linejoin: round; }
201
+
202
+ /* ---------- 引用 ---------- */
203
+ #quote-wrap { position: relative; margin: 12px 0 2px; }
204
+ #quote {
205
+ margin: 0;
206
+ padding: 0 0 0 12px;
207
+ border-left: 2px solid var(--blue-line);
208
+ color: var(--muted);
209
+ line-height: 1.6;
210
+ white-space: pre-wrap;
211
+ word-break: break-word;
212
+ display: -webkit-box;
213
+ -webkit-line-clamp: 3;
214
+ -webkit-box-orient: vertical;
215
+ overflow: hidden;
216
+ cursor: pointer;
217
+ }
218
+ #quote.open { display: block; -webkit-line-clamp: unset; max-height: 220px; overflow: auto; }
219
+
220
+ /* 截图缩略图 */
221
+ #shot { position: relative; align-self: flex-start; display: flex; gap: 8px; margin: 12px 0 0; }
222
+ #shot img { display: block; max-width: 220px; max-height: 96px; border-radius: 8px; border: 1px solid var(--line); }
223
+ /* 引用和截图右上角的 ×:悬停才出现,点掉就不发 */
224
+ .drop {
225
+ position: absolute; top: -7px; right: -7px;
226
+ width: 18px; height: 18px; padding: 0;
227
+ display: flex; align-items: center; justify-content: center;
228
+ border: none; border-radius: 50%;
229
+ background: var(--accent); color: var(--accent-text);
230
+ font-size: 12px; line-height: 1; cursor: pointer;
231
+ opacity: 0; transition: opacity 120ms;
232
+ }
233
+ #shot:hover .drop, #quote-wrap:hover .drop { opacity: 1; }
234
+
235
+ /* ---------- 对话 ---------- */
236
+ #thread { margin-top: 12px; flex: 1; min-height: 0; overflow: auto; user-select: text; }
237
+ .you { margin: 0 0 10px auto; width: fit-content; max-width: 85%; padding: 6px 12px; border-radius: 14px 14px 4px 14px; background: var(--blue-soft); line-height: 1.5; white-space: pre-wrap; word-break: break-word; }
238
+ .reply { margin-bottom: 10px; line-height: 1.65; white-space: pre-wrap; word-break: break-word; }
239
+ .reply code { font-family: "Cascadia Mono", Consolas, monospace; font-size: 12.5px; background: var(--code); padding: 1px 4px; border-radius: 4px; }
240
+ .reply pre { margin: 6px 0; padding: 8px 10px; background: var(--code); border-radius: 8px; overflow-x: auto; white-space: pre; }
241
+ .reply pre code { background: none; padding: 0; }
242
+ .reply .h { font-size: 15px; }
243
+ .reply table { border-collapse: collapse; margin: 4px 0; font-size: 13px; white-space: normal; }
244
+ .reply th, .reply td { border-bottom: 1px solid var(--line); padding: 4px 10px 4px 0; text-align: left; vertical-align: top; }
245
+ .reply th { color: var(--muted); font-weight: 600; }
246
+ .reply blockquote { margin: 4px 0; padding: 0 0 0 10px; border-left: 2px solid var(--quote); color: var(--muted); }
247
+ .reply a { color: inherit; text-decoration: underline; text-decoration-color: var(--faint); text-underline-offset: 2px; cursor: pointer; word-break: break-all; }
248
+ .after { margin: -4px 0 8px; font-size: 12.5px; color: var(--faint); }
249
+ .after button { border: none; background: none; padding: 0; color: var(--faint); cursor: pointer; }
250
+ .after button:hover { color: var(--text); }
251
+ #activity { margin: 2px 0 6px; font-size: 12.5px; color: var(--faint); }
252
+ #activity.error { color: var(--error); }
253
+
254
+ /* ---------- 输入区 ---------- */
255
+ #composer { margin-top: 10px; }
256
+ #input {
257
+ box-sizing: border-box;
258
+ width: 100%;
259
+ min-height: 54px;
260
+ max-height: 160px;
261
+ padding: 4px 0;
262
+ resize: none;
263
+ border: none;
264
+ outline: none;
265
+ background: transparent;
266
+ color: var(--text);
267
+ font: inherit;
268
+ font-size: 15.5px;
269
+ line-height: 1.5;
270
+ }
271
+ #input::placeholder { color: var(--faint); }
272
+ #input { caret-color: var(--blue); }
273
+
274
+ /* 建议:输入框空的时候才出现,极轻的灰字 */
275
+ #suggest { display: flex; flex-wrap: wrap; gap: 6px; margin: 2px 0 4px; }
276
+ #suggest button {
277
+ padding: 3px 10px;
278
+ border: none;
279
+ border-radius: 12px;
280
+ background: var(--chip);
281
+ color: var(--muted);
282
+ font-size: 12.5px;
283
+ cursor: pointer;
284
+ max-width: 100%;
285
+ white-space: nowrap;
286
+ overflow: hidden;
287
+ text-overflow: ellipsis;
288
+ }
289
+ #suggest button:hover { background: var(--blue-soft); color: var(--blue); }
290
+ #suggest .skeleton { width: 92px; height: 22px; border-radius: 12px; background: linear-gradient(90deg, var(--chip) 25%, var(--chip-hover) 50%, var(--chip) 75%); background-size: 200% 100%; animation: shimmer 1.2s infinite; }
291
+ #suggest .skeleton + .skeleton { width: 70px; }
292
+ @keyframes shimmer { from { background-position: 200% 0; } to { background-position: -200% 0; } }
293
+
294
+ #bar { display: flex; align-items: center; gap: 10px; margin-top: 8px; }
295
+ /* 模型:和发送目标一样的小下拉,菜单往上开;文字和上面左对齐 */
296
+ #model-wrap { position: relative; min-width: 0; margin-left: -6px; }
297
+ #model {
298
+ display: inline-flex; align-items: center; gap: 4px;
299
+ max-width: 220px;
300
+ padding: 3px 6px;
301
+ border: none; border-radius: 6px; background: transparent;
302
+ color: var(--muted); font-size: 13px; cursor: pointer;
303
+ }
304
+ #model-label { min-width: 0; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
305
+ #model svg { flex: none; width: 10px; height: 10px; fill: none; stroke: currentColor; stroke-width: 1.3; }
306
+ #model:hover { background: var(--chip); color: var(--text); }
307
+ #model:disabled { cursor: default; background: transparent; }
308
+ #model:disabled svg { display: none; }
309
+ #model-menu {
310
+ position: absolute; left: -6px; bottom: 32px; z-index: 10;
311
+ width: 260px; max-height: 260px; overflow: auto; padding: 6px;
312
+ border-radius: 12px;
313
+ background: var(--bg);
314
+ box-shadow: var(--shadow);
315
+ border: 1px solid var(--line);
316
+ }
317
+ .group { padding: 6px 8px 2px; color: var(--faint); font-size: 11.5px; }
318
+ /* 模型菜单底下的思考强度:一行分段按钮,样子同快答/深入 */
319
+ .effort-row { display: flex; align-items: center; gap: 8px; padding: 4px 6px 2px 8px; }
320
+ .effort-label { flex: none; color: var(--faint); font-size: 12px; }
321
+ .effort-seg { display: inline-flex; flex-wrap: wrap; padding: 2px; border-radius: 8px; background: var(--chip); }
322
+ .effort-seg button { padding: 2px 8px; border: none; border-radius: 6px; background: transparent; color: var(--faint); font-size: 12.5px; cursor: pointer; }
323
+ .effort-seg button:hover { color: var(--text); }
324
+ .effort-seg button.on { background: var(--bg); color: var(--text); box-shadow: 0 1px 2px rgba(15, 23, 42, 0.12); }
325
+
326
+ /* 快答 / 深入:小号分段开关,选中的那格浮起来 */
327
+ #mode { flex: none; display: inline-flex; padding: 2px; border-radius: 8px; background: var(--chip); }
328
+ #mode button {
329
+ padding: 2px 9px; border: none; border-radius: 6px; background: transparent;
330
+ color: var(--faint); font-size: 12.5px; cursor: pointer;
331
+ }
332
+ #mode button:hover { color: var(--text); }
333
+ #mode button.on { background: var(--bg); color: var(--text); box-shadow: 0 1px 2px rgba(15, 23, 42, 0.12); }
334
+
335
+ /* 拖过大小之后:还没提问时输入框占满剩下的空间,发送栏贴底;提问后由对话区占满 */
336
+ #card.manual #thread[hidden] ~ #composer { flex: 1 1 auto; display: flex; flex-direction: column; min-height: 0; }
337
+ #card.manual #thread[hidden] ~ #composer #input { flex: 1 1 auto; max-height: none; }
338
+
339
+ /* 右下角调大小 */
340
+ #grip {
341
+ position: absolute; right: 3px; bottom: 3px;
342
+ width: 16px; height: 16px;
343
+ display: flex; align-items: center; justify-content: center;
344
+ color: var(--faint); opacity: 0.7;
345
+ cursor: nwse-resize;
346
+ }
347
+ #grip:hover { opacity: 1; color: var(--muted); }
348
+ #grip svg { width: 11px; height: 11px; fill: none; stroke: currentColor; stroke-width: 1.5; stroke-linecap: round; stroke-linejoin: round; }
349
+ #send {
350
+ width: 32px; height: 32px;
351
+ display: inline-flex; align-items: center; justify-content: center;
352
+ border: none; border-radius: 50%;
353
+ background: var(--blue); color: #fff;
354
+ cursor: pointer;
355
+ transition: background 120ms, opacity 120ms, transform 120ms;
356
+ }
357
+ #send:not(:disabled):hover { filter: brightness(1.06); }
358
+ #send:not(:disabled):active { transform: scale(0.94); }
359
+ #send:disabled { background: var(--chip-hover); color: var(--faint); cursor: default; }
360
+ #send svg { width: 15px; height: 15px; fill: none; stroke: currentColor; stroke-width: 1.8; stroke-linecap: round; stroke-linejoin: round; }
361
+
362
+ /* ---------- 配对 ---------- */
363
+ #pair-view { padding: 10px 0 4px; text-align: center; color: var(--muted); }
364
+ #pair-code { font-size: 34px; font-weight: 600; letter-spacing: 8px; margin: 8px 0; color: var(--text); }
365
+ .hint { font-size: 12.5px; color: var(--faint); }
@@ -0,0 +1,68 @@
1
+ <!doctype html>
2
+ <html lang="zh-CN">
3
+ <head>
4
+ <meta charset="utf-8">
5
+ <meta http-equiv="Content-Security-Policy" content="default-src 'self'; img-src 'self' data:; style-src 'self'">
6
+ <title>Pointy</title>
7
+ <link rel="stylesheet" href="card.css">
8
+ </head>
9
+ <body>
10
+ <!-- 看板娘:扒在卡片左边往里看;在卡片外面,所以不受卡片的 overflow 裁切 -->
11
+ <div id="mascot" class="idle" aria-hidden="true">
12
+ <img alt="" draggable="false">
13
+ </div>
14
+ <div id="card">
15
+ <header id="drag">
16
+ <span id="status" title="空闲"></span>
17
+ <div id="target-wrap">
18
+ <button id="target" type="button"><span id="target-label">…</span><span id="target-meta"></span><svg viewBox="0 0 10 10"><path d="M2 3.5 5 6.5 8 3.5"/></svg></button>
19
+ <div id="target-menu" hidden></div>
20
+ </div>
21
+ <button id="new-task" class="icon" type="button" title="改成新对话(Tab)" hidden>
22
+ <svg viewBox="0 0 16 16"><path d="M8 3.5v9M3.5 8h9"/></svg>
23
+ </button>
24
+ <span class="spacer"></span>
25
+ <button id="open-dsh" class="icon" type="button" title="在 DSH 中打开">
26
+ <svg viewBox="0 0 16 16"><path d="M6 3h7v7M13 3 4 12"/></svg>
27
+ </button>
28
+ <button id="close" class="icon" type="button" title="关闭(Esc)">
29
+ <svg viewBox="0 0 16 16"><path d="M4 4l8 8M12 4l-8 8"/></svg>
30
+ </button>
31
+ </header>
32
+
33
+ <div id="shot" hidden><img alt="指到的那块的截图"><img class="wide" alt="整个区域的截图" hidden><button class="drop" type="button" title="不发这张图">×</button></div>
34
+ <div id="quote-wrap" hidden><blockquote id="quote" title="点一下展开"></blockquote><button class="drop" type="button" title="不发这段内容">×</button></div>
35
+
36
+ <section id="thread" hidden></section>
37
+ <div id="activity" hidden></div>
38
+
39
+ <div id="composer">
40
+ <textarea id="input" rows="1" placeholder="想问点什么…"></textarea>
41
+ <div id="suggest"></div>
42
+ <div id="bar">
43
+ <div id="model-wrap">
44
+ <button id="model" type="button" title="这次用的模型"><span id="model-label">DeepSeek</span><svg viewBox="0 0 10 10"><path d="M2 3.5 5 6.5 8 3.5"/></svg></button>
45
+ <div id="model-menu" hidden></div>
46
+ </div>
47
+ <div id="mode" role="radiogroup" aria-label="回答方式">
48
+ <button type="button" data-mode="quick" title="快答:直接简短回答,不跑命令、不下载,最多搜一两次网页">快答</button>
49
+ <button type="button" data-mode="deep" title="深入:不加限制,DSH 该查就查、该跑就跑">深入</button>
50
+ </div>
51
+ <span class="spacer"></span>
52
+ <button id="send" type="button" title="发送(Enter)" disabled>
53
+ <svg viewBox="0 0 16 16"><path d="M8 13V3M3.5 7.5 8 3l4.5 4.5"/></svg>
54
+ </button>
55
+ </div>
56
+ </div>
57
+
58
+ <div id="grip" title="拖动调整大小"><svg viewBox="0 0 12 12"><path d="M10 4v6H4"/></svg></div>
59
+
60
+ <section id="pair-view" hidden>
61
+ <p>在浏览器扩展的弹窗里输入这个配对码:</p>
62
+ <div id="pair-code"></div>
63
+ <p class="hint">两分钟内有效,只能用一次。</p>
64
+ </section>
65
+ </div>
66
+ <script type="module" src="card.js"></script>
67
+ </body>
68
+ </html>