dsh-quicknote 0.0.0-stage → 0.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/AGENTS.md +28 -0
- package/CHANGELOG.md +11 -0
- package/LICENSE +21 -0
- package/PUBLISHING.md +11 -0
- package/README.md +62 -2
- package/README.zh.md +61 -0
- package/cordis.patch.yml +5 -0
- package/icon.svg +4 -0
- package/lib/client.js +534 -0
- package/lib/index.js +275 -0
- package/llms.txt +30 -0
- package/locale/en.json +6 -0
- package/locale/zh.json +6 -0
- package/package.json +76 -4
package/AGENTS.md
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# dsh-quicknote — agent notes
|
|
2
|
+
|
|
3
|
+
Read this before changing code. User docs: [README.md](README.md).
|
|
4
|
+
|
|
5
|
+
## Commands
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
pnpm install
|
|
9
|
+
pnpm verify # format → lint → typecheck → test → build → pack
|
|
10
|
+
pnpm typecheck
|
|
11
|
+
pnpm test
|
|
12
|
+
pnpm build
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
## Layout
|
|
16
|
+
|
|
17
|
+
- `src/host/` — Host half: services, tools, routes. No browser APIs.
|
|
18
|
+
- `src/client/` — browser half: React 19 components (TSX), slots, styles. No Node APIs.
|
|
19
|
+
- `src/shared/` — constants both halves import (`NAME`, `NS`, `PLUGIN_ID`).
|
|
20
|
+
- `lib/` — generated. Never edit by hand.
|
|
21
|
+
- `tests/` — one suite per half.
|
|
22
|
+
|
|
23
|
+
## Invariants
|
|
24
|
+
|
|
25
|
+
- `package.json` `name` === `src/shared/constants.ts` `NAME` === the client bundle id.
|
|
26
|
+
- The `window.__ModuleLoader__.load` wrapper is generated by `tsdown.config.ts`; source must not contain it.
|
|
27
|
+
- No network calls, no telemetry, no `console.log` in shipped code.
|
|
28
|
+
- Any destructive action must be reversible and must state its consequence.
|
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## 0.1.0
|
|
4
|
+
|
|
5
|
+
First release.
|
|
6
|
+
|
|
7
|
+
- Composer toolbar button with a capture card prefilled from the current draft
|
|
8
|
+
- Save note, or save and explicitly clear the draft
|
|
9
|
+
- Sidebar page: newest-first list, done/reopen, delete
|
|
10
|
+
- Four Agent tools: add, list, done, delete
|
|
11
|
+
- Local JSON store at `~/.dsh/quicknote/notes.json` (0600, atomic writes)
|
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 dsh-kit contributors
|
|
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/PUBLISHING.md
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
# Publishing
|
|
2
|
+
|
|
3
|
+
1. Run `pnpm verify` and make sure it is green.
|
|
4
|
+
2. Bump `version` and add the matching `CHANGELOG.md` entry.
|
|
5
|
+
3. Inspect the artifact: `npm pack --dry-run`.
|
|
6
|
+
4. Publish with provenance: `npm publish --provenance`.
|
|
7
|
+
5. Tag the release: `git tag v<version>` and push the tag.
|
|
8
|
+
6. Install the published package into a fresh profile and check the client bundle is served.
|
|
9
|
+
|
|
10
|
+
For local development, `dsh plugin --profile <profile> add link:$PWD` installs the working copy instead of a
|
|
11
|
+
published version.
|
package/README.md
CHANGED
|
@@ -1,3 +1,63 @@
|
|
|
1
|
-
#
|
|
1
|
+
# dsh-quicknote
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Capture a note from the composer, keep it out of the way, and find it again on one page.
|
|
4
|
+
|
|
5
|
+
- **From the composer**: click **Note** in the composer toolbar. The card opens with the current draft; save it
|
|
6
|
+
as a note, or save and explicitly clear the draft.
|
|
7
|
+
- **From the sidebar**: the Quick Note page lists everything newest-first, with done/reopen and delete.
|
|
8
|
+
- **From an agent**: `quicknote_add`, `quicknote_list`, `quicknote_done`, `quicknote_delete`.
|
|
9
|
+
- **Storage**: `~/.dsh/quicknote/notes.json` — local, mode `0600`, atomic writes. No telemetry.
|
|
10
|
+
|
|
11
|
+
## Documentation
|
|
12
|
+
|
|
13
|
+
<https://dsh-kit.github.io/dsh-quicknote/>
|
|
14
|
+
|
|
15
|
+
## Install
|
|
16
|
+
|
|
17
|
+
For development, link the working copy:
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
dsh plugin --profile <profile> add link:$PWD
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
A published release installs by package name:
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
dsh plugin --profile <profile> add dsh-quicknote
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
Desktop profiles are managed by the app: add `dsh-quicknote` to `dsh.profile.bundles` and the dependency, run
|
|
30
|
+
`pnpm install` in the profile, then restart DSH.
|
|
31
|
+
|
|
32
|
+
## Verify
|
|
33
|
+
|
|
34
|
+
```bash
|
|
35
|
+
dsh --profile <profile> --dump-config | grep -A2 "id: dsh-quicknote"
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
## Develop
|
|
39
|
+
|
|
40
|
+
```bash
|
|
41
|
+
pnpm install
|
|
42
|
+
pnpm verify # format → lint → typecheck → tests → build → pack
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
npm works too: `npm install && npm run verify`.
|
|
46
|
+
|
|
47
|
+
- `src/host/` — store, four JSON routes and four Agent tools.
|
|
48
|
+
- `src/client/` — composer button, capture card, sidebar page.
|
|
49
|
+
- `lib/` — generated, committed (a `link:` install never builds).
|
|
50
|
+
- `tests/` — store and client registration.
|
|
51
|
+
- `AGENTS.md` / `llms.txt` — agent context.
|
|
52
|
+
- `PUBLISHING.md` — release checklist.
|
|
53
|
+
|
|
54
|
+
## Data and API
|
|
55
|
+
|
|
56
|
+
- `GET /api/quicknote/notes`
|
|
57
|
+
- `POST /api/quicknote/notes/create` `{ text }`
|
|
58
|
+
- `POST /api/quicknote/notes/update` `{ id, done }`
|
|
59
|
+
- `POST /api/quicknote/notes/delete` `{ id }`
|
|
60
|
+
|
|
61
|
+
## License
|
|
62
|
+
|
|
63
|
+
MIT
|
package/README.zh.md
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
# dsh-quicknote
|
|
2
|
+
|
|
3
|
+
从输入框随手记下一条,不打断当前对话,之后在一页里找回。
|
|
4
|
+
|
|
5
|
+
- **从输入框**:点击工具栏的 **记一下**;卡片会带上当前草稿。可以只保存笔记,也可以显式选择“保存并清空输入框”。
|
|
6
|
+
- **从侧栏**:随手记页面按时间倒序列出全部笔记,可完成、重新打开、删除。
|
|
7
|
+
- **从 Agent**:`quicknote_add`、`quicknote_list`、`quicknote_done`、`quicknote_delete`。
|
|
8
|
+
- **存储**:`~/.dsh/quicknote/notes.json`,本地、权限 `0600`、原子写入,无遥测。
|
|
9
|
+
|
|
10
|
+
## 文档
|
|
11
|
+
|
|
12
|
+
<https://dsh-kit.github.io/dsh-quicknote/zh/>
|
|
13
|
+
|
|
14
|
+
## 安装
|
|
15
|
+
|
|
16
|
+
开发时链接工作副本:
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
dsh plugin --profile <profile> add link:$PWD
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
发布后按包名安装:
|
|
23
|
+
|
|
24
|
+
```bash
|
|
25
|
+
dsh plugin --profile <profile> add dsh-quicknote
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
桌面 profile 由应用托管:把 `dsh-quicknote` 加进 `dsh.profile.bundles` 与依赖,在 profile 里执行 `pnpm install`,重启 DSH。
|
|
29
|
+
|
|
30
|
+
## 验证
|
|
31
|
+
|
|
32
|
+
```bash
|
|
33
|
+
dsh --profile <profile> --dump-config | grep -A2 "id: dsh-quicknote"
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
## 开发
|
|
37
|
+
|
|
38
|
+
```bash
|
|
39
|
+
pnpm install
|
|
40
|
+
pnpm verify # 格式 → lint → 类型 → 测试 → 构建 → 打包
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
npm 也可用:`npm install && npm run verify`。
|
|
44
|
+
|
|
45
|
+
- `src/host/`:存储、四个 JSON 路由、四个 Agent 工具;
|
|
46
|
+
- `src/client/`:输入框按钮、记录卡片、侧栏页面;
|
|
47
|
+
- `lib/`:构建产物,提交进仓库(`link:` 安装不会构建);
|
|
48
|
+
- `tests/`:存储与客户端注册测试;
|
|
49
|
+
- `AGENTS.md` / `llms.txt`:agent 上下文;
|
|
50
|
+
- `PUBLISHING.md`:发布清单。
|
|
51
|
+
|
|
52
|
+
## 数据与 API
|
|
53
|
+
|
|
54
|
+
- `GET /api/quicknote/notes`
|
|
55
|
+
- `POST /api/quicknote/notes/create` `{ text }`
|
|
56
|
+
- `POST /api/quicknote/notes/update` `{ id, done }`
|
|
57
|
+
- `POST /api/quicknote/notes/delete` `{ id }`
|
|
58
|
+
|
|
59
|
+
## 许可
|
|
60
|
+
|
|
61
|
+
MIT
|
package/cordis.patch.yml
ADDED
package/icon.svg
ADDED
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="24" height="24" fill="none">
|
|
2
|
+
<rect x="4" y="4" width="16" height="16" rx="4" stroke="currentColor" stroke-width="1.6"/>
|
|
3
|
+
<path d="M8 12h8" stroke="currentColor" stroke-width="1.6" stroke-linecap="round"/>
|
|
4
|
+
</svg>
|
package/lib/client.js
ADDED
|
@@ -0,0 +1,534 @@
|
|
|
1
|
+
window.__ModuleLoader__.load({
|
|
2
|
+
id: "dsh-quicknote",
|
|
3
|
+
factory: (require) => {
|
|
4
|
+
var module = { exports: {} };
|
|
5
|
+
var exports = module.exports;
|
|
6
|
+
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
7
|
+
let react = require("react");
|
|
8
|
+
let react_jsx_runtime = require("react/jsx-runtime");
|
|
9
|
+
// src/shared/constants.ts
|
|
10
|
+
/** Package name; must equal package.json `name` (the client bundle id). */
|
|
11
|
+
const NAME = "dsh-quicknote";
|
|
12
|
+
/** Locale namespace owned by this plugin. */
|
|
13
|
+
const NS = "dsh-quicknote";
|
|
14
|
+
/** Host JSON API used by the page. */
|
|
15
|
+
const API = "/api/quicknote";
|
|
16
|
+
// src/client/session.ts
|
|
17
|
+
/** Access to the per-Session composer owned by the conversation service. */
|
|
18
|
+
let clientCtx = null;
|
|
19
|
+
function setClientContext(ctx) {
|
|
20
|
+
clientCtx = ctx;
|
|
21
|
+
}
|
|
22
|
+
function sessionIdNear(node) {
|
|
23
|
+
try {
|
|
24
|
+
const id = (typeof node?.closest === "function" ? node.closest("[data-conversation-session]") : null)?.getAttribute("data-conversation-session");
|
|
25
|
+
return typeof id === "string" && id !== "" ? id : void 0;
|
|
26
|
+
} catch {
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
function composerOf(sessionId) {
|
|
31
|
+
if (sessionId === void 0) return void 0;
|
|
32
|
+
try {
|
|
33
|
+
const input = clientCtx?.get("conversation")?.input;
|
|
34
|
+
if (typeof input?.shell !== "function") return void 0;
|
|
35
|
+
const shell = input.shell(sessionId);
|
|
36
|
+
if (!shell?.state || !shell?.actions) return void 0;
|
|
37
|
+
return shell;
|
|
38
|
+
} catch {
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
function draftOf(sessionId) {
|
|
43
|
+
try {
|
|
44
|
+
return String(composerOf(sessionId)?.state.getSnapshot().draft ?? "");
|
|
45
|
+
} catch {
|
|
46
|
+
return "";
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
// src/client/ComposerButton.tsx
|
|
50
|
+
/** A labelled button in the composer toolbar. No icon-only destructive control. */
|
|
51
|
+
function ComposerButton({ t }) {
|
|
52
|
+
function open(event) {
|
|
53
|
+
const sessionId = sessionIdNear(event.currentTarget);
|
|
54
|
+
window.dispatchEvent(new CustomEvent("dsh-quicknote:open", { detail: {
|
|
55
|
+
sessionId,
|
|
56
|
+
text: draftOf(sessionId)
|
|
57
|
+
} }));
|
|
58
|
+
}
|
|
59
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
|
|
60
|
+
type: "button",
|
|
61
|
+
onClick: open,
|
|
62
|
+
"data-testid": "quicknote-composer",
|
|
63
|
+
style: {
|
|
64
|
+
display: "inline-flex",
|
|
65
|
+
alignItems: "center",
|
|
66
|
+
gap: 6,
|
|
67
|
+
padding: "5px 9px",
|
|
68
|
+
borderRadius: 8,
|
|
69
|
+
border: "1px solid var(--dsw-alias-line, #d9dce1)",
|
|
70
|
+
background: "transparent",
|
|
71
|
+
color: "inherit",
|
|
72
|
+
font: "inherit",
|
|
73
|
+
cursor: "pointer"
|
|
74
|
+
},
|
|
75
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("svg", {
|
|
76
|
+
viewBox: "0 0 16 16",
|
|
77
|
+
width: "16",
|
|
78
|
+
height: "16",
|
|
79
|
+
fill: "none",
|
|
80
|
+
"aria-hidden": "true",
|
|
81
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("rect", {
|
|
82
|
+
x: "2.75",
|
|
83
|
+
y: "2.75",
|
|
84
|
+
width: "10.5",
|
|
85
|
+
height: "10.5",
|
|
86
|
+
rx: "2",
|
|
87
|
+
stroke: "currentColor"
|
|
88
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", {
|
|
89
|
+
d: "M5.5 8h5",
|
|
90
|
+
stroke: "currentColor",
|
|
91
|
+
strokeLinecap: "round"
|
|
92
|
+
})]
|
|
93
|
+
}), t("toolbar.capture")]
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
// src/client/Overlay.tsx
|
|
97
|
+
/** Capture card in the shell overlay; saves a note and optionally clears the draft. */
|
|
98
|
+
function Overlay({ t }) {
|
|
99
|
+
const [open, setOpen] = (0, react.useState)(false);
|
|
100
|
+
const [text, setText] = (0, react.useState)("");
|
|
101
|
+
const [sessionId, setSessionId] = (0, react.useState)(void 0);
|
|
102
|
+
const [error, setError] = (0, react.useState)("");
|
|
103
|
+
const [saving, setSaving] = (0, react.useState)(false);
|
|
104
|
+
(0, react.useEffect)(() => {
|
|
105
|
+
const onOpen = (event) => {
|
|
106
|
+
const detail = event.detail;
|
|
107
|
+
setText(detail?.text ?? "");
|
|
108
|
+
setSessionId(detail?.sessionId);
|
|
109
|
+
setError("");
|
|
110
|
+
setOpen(true);
|
|
111
|
+
};
|
|
112
|
+
window.addEventListener("dsh-quicknote:open", onOpen);
|
|
113
|
+
return () => window.removeEventListener("dsh-quicknote:open", onOpen);
|
|
114
|
+
}, []);
|
|
115
|
+
(0, react.useEffect)(() => {
|
|
116
|
+
if (!open) return;
|
|
117
|
+
const onKey = (event) => {
|
|
118
|
+
if (event.key === "Escape") setOpen(false);
|
|
119
|
+
};
|
|
120
|
+
window.addEventListener("keydown", onKey);
|
|
121
|
+
return () => window.removeEventListener("keydown", onKey);
|
|
122
|
+
}, [open]);
|
|
123
|
+
async function save(clearDraft) {
|
|
124
|
+
if (text.trim() === "" || saving) return;
|
|
125
|
+
setSaving(true);
|
|
126
|
+
setError("");
|
|
127
|
+
try {
|
|
128
|
+
const response = await fetch(`${API}/notes/create`, {
|
|
129
|
+
method: "POST",
|
|
130
|
+
headers: { "content-type": "application/json" },
|
|
131
|
+
body: JSON.stringify({ text })
|
|
132
|
+
});
|
|
133
|
+
const result = await response.json();
|
|
134
|
+
if (!response.ok || !result.ok) throw new Error(result.error ?? "save-failed");
|
|
135
|
+
if (clearDraft && sessionId !== void 0) try {
|
|
136
|
+
composerOf(sessionId)?.actions.setDraft("");
|
|
137
|
+
} catch {}
|
|
138
|
+
setOpen(false);
|
|
139
|
+
window.dispatchEvent(new Event("dsh-quicknote:changed"));
|
|
140
|
+
} catch (cause) {
|
|
141
|
+
setError(String(cause.message));
|
|
142
|
+
} finally {
|
|
143
|
+
setSaving(false);
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
if (!open) return null;
|
|
147
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
148
|
+
style: {
|
|
149
|
+
position: "fixed",
|
|
150
|
+
inset: 0,
|
|
151
|
+
zIndex: 50,
|
|
152
|
+
display: "flex",
|
|
153
|
+
alignItems: "center",
|
|
154
|
+
justifyContent: "center",
|
|
155
|
+
background: "rgba(15,17,21,.45)",
|
|
156
|
+
padding: 24
|
|
157
|
+
},
|
|
158
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
159
|
+
role: "dialog",
|
|
160
|
+
"aria-modal": "true",
|
|
161
|
+
"aria-label": t("overlay.title"),
|
|
162
|
+
style: {
|
|
163
|
+
width: "min(560px, 100%)",
|
|
164
|
+
padding: 20,
|
|
165
|
+
borderRadius: 14,
|
|
166
|
+
background: "var(--dsw-alias-bg, #fff)",
|
|
167
|
+
boxShadow: "0 24px 70px rgba(0,0,0,.3)"
|
|
168
|
+
},
|
|
169
|
+
children: [
|
|
170
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("h2", {
|
|
171
|
+
style: {
|
|
172
|
+
margin: 0,
|
|
173
|
+
fontSize: 18
|
|
174
|
+
},
|
|
175
|
+
children: t("overlay.title")
|
|
176
|
+
}),
|
|
177
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("textarea", {
|
|
178
|
+
"data-testid": "quicknote-overlay-input",
|
|
179
|
+
autoFocus: true,
|
|
180
|
+
value: text,
|
|
181
|
+
onChange: (event) => setText(event.target.value),
|
|
182
|
+
rows: 5,
|
|
183
|
+
style: {
|
|
184
|
+
width: "100%",
|
|
185
|
+
boxSizing: "border-box",
|
|
186
|
+
marginTop: 12,
|
|
187
|
+
padding: 10,
|
|
188
|
+
borderRadius: 8,
|
|
189
|
+
border: "1px solid var(--dsw-alias-line, #d9dce1)",
|
|
190
|
+
font: "inherit",
|
|
191
|
+
resize: "vertical"
|
|
192
|
+
}
|
|
193
|
+
}),
|
|
194
|
+
error === "" ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
195
|
+
role: "alert",
|
|
196
|
+
style: {
|
|
197
|
+
color: "var(--dsw-alias-danger, #c0392b)",
|
|
198
|
+
margin: "8px 0 0"
|
|
199
|
+
},
|
|
200
|
+
children: error
|
|
201
|
+
}),
|
|
202
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
203
|
+
style: {
|
|
204
|
+
display: "flex",
|
|
205
|
+
justifyContent: "flex-end",
|
|
206
|
+
gap: 8,
|
|
207
|
+
marginTop: 14,
|
|
208
|
+
flexWrap: "wrap"
|
|
209
|
+
},
|
|
210
|
+
children: [
|
|
211
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
212
|
+
type: "button",
|
|
213
|
+
onClick: () => setOpen(false),
|
|
214
|
+
style: {
|
|
215
|
+
padding: "7px 12px",
|
|
216
|
+
borderRadius: 8
|
|
217
|
+
},
|
|
218
|
+
children: t("overlay.cancel")
|
|
219
|
+
}),
|
|
220
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
221
|
+
type: "button",
|
|
222
|
+
disabled: saving,
|
|
223
|
+
onClick: () => void save(false),
|
|
224
|
+
style: {
|
|
225
|
+
padding: "7px 12px",
|
|
226
|
+
borderRadius: 8
|
|
227
|
+
},
|
|
228
|
+
children: t("overlay.save")
|
|
229
|
+
}),
|
|
230
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
231
|
+
type: "button",
|
|
232
|
+
disabled: saving || sessionId === void 0,
|
|
233
|
+
onClick: () => void save(true),
|
|
234
|
+
style: {
|
|
235
|
+
padding: "7px 12px",
|
|
236
|
+
borderRadius: 8
|
|
237
|
+
},
|
|
238
|
+
children: t("overlay.saveClear")
|
|
239
|
+
})
|
|
240
|
+
]
|
|
241
|
+
})
|
|
242
|
+
]
|
|
243
|
+
})
|
|
244
|
+
});
|
|
245
|
+
}
|
|
246
|
+
// src/client/index.tsx
|
|
247
|
+
/**
|
|
248
|
+
* Browser half of the Quick Note example. React is provided by DSH at runtime.
|
|
249
|
+
*
|
|
250
|
+
* @module dsh-quicknote/client
|
|
251
|
+
*/
|
|
252
|
+
async function request(path, init) {
|
|
253
|
+
const response = await fetch(`${API}${path}`, {
|
|
254
|
+
headers: { "content-type": "application/json" },
|
|
255
|
+
...init
|
|
256
|
+
});
|
|
257
|
+
const data = await response.json();
|
|
258
|
+
if (!response.ok || !data.ok) throw new Error(data.error ?? `request-failed-${response.status}`);
|
|
259
|
+
return data;
|
|
260
|
+
}
|
|
261
|
+
function Icon({ size = 18 }) {
|
|
262
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("svg", {
|
|
263
|
+
viewBox: "0 0 16 16",
|
|
264
|
+
width: size,
|
|
265
|
+
height: size,
|
|
266
|
+
fill: "none",
|
|
267
|
+
"aria-hidden": "true",
|
|
268
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("rect", {
|
|
269
|
+
x: "2.75",
|
|
270
|
+
y: "2.75",
|
|
271
|
+
width: "10.5",
|
|
272
|
+
height: "10.5",
|
|
273
|
+
rx: "2",
|
|
274
|
+
stroke: "currentColor"
|
|
275
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", {
|
|
276
|
+
d: "M5.5 8h5",
|
|
277
|
+
stroke: "currentColor",
|
|
278
|
+
strokeLinecap: "round"
|
|
279
|
+
})]
|
|
280
|
+
});
|
|
281
|
+
}
|
|
282
|
+
function Page({ t }) {
|
|
283
|
+
const [notes, setNotes] = (0, react.useState)([]);
|
|
284
|
+
const [text, setText] = (0, react.useState)("");
|
|
285
|
+
const [error, setError] = (0, react.useState)("");
|
|
286
|
+
const [undo, setUndo] = (0, react.useState)(null);
|
|
287
|
+
async function load() {
|
|
288
|
+
try {
|
|
289
|
+
const data = await request("/notes");
|
|
290
|
+
setNotes(data.notes);
|
|
291
|
+
} catch (cause) {
|
|
292
|
+
setError(String(cause.message));
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
(0, react.useEffect)(() => {
|
|
296
|
+
load();
|
|
297
|
+
const changed = () => void load();
|
|
298
|
+
window.addEventListener("dsh-quicknote:changed", changed);
|
|
299
|
+
return () => window.removeEventListener("dsh-quicknote:changed", changed);
|
|
300
|
+
}, []);
|
|
301
|
+
(0, react.useEffect)(() => {
|
|
302
|
+
if (!undo) return;
|
|
303
|
+
const timer = window.setTimeout(() => setUndo(null), 8e3);
|
|
304
|
+
return () => window.clearTimeout(timer);
|
|
305
|
+
}, [undo]);
|
|
306
|
+
async function add(event) {
|
|
307
|
+
event.preventDefault();
|
|
308
|
+
if (text.trim() === "") return;
|
|
309
|
+
try {
|
|
310
|
+
await request("/notes/create", {
|
|
311
|
+
method: "POST",
|
|
312
|
+
body: JSON.stringify({ text })
|
|
313
|
+
});
|
|
314
|
+
setText("");
|
|
315
|
+
setError("");
|
|
316
|
+
await load();
|
|
317
|
+
} catch (cause) {
|
|
318
|
+
setError(String(cause.message));
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
async function toggle(note) {
|
|
322
|
+
try {
|
|
323
|
+
await request("/notes/update", {
|
|
324
|
+
method: "POST",
|
|
325
|
+
body: JSON.stringify({
|
|
326
|
+
id: note.id,
|
|
327
|
+
done: !note.done
|
|
328
|
+
})
|
|
329
|
+
});
|
|
330
|
+
await load();
|
|
331
|
+
} catch (cause) {
|
|
332
|
+
setError(String(cause.message));
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
async function remove(note) {
|
|
336
|
+
try {
|
|
337
|
+
await request("/notes/delete", {
|
|
338
|
+
method: "POST",
|
|
339
|
+
body: JSON.stringify({ id: note.id })
|
|
340
|
+
});
|
|
341
|
+
await load();
|
|
342
|
+
} catch (cause) {
|
|
343
|
+
setError(String(cause.message));
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
347
|
+
style: {
|
|
348
|
+
padding: "24px 28px",
|
|
349
|
+
maxWidth: 760,
|
|
350
|
+
font: "inherit"
|
|
351
|
+
},
|
|
352
|
+
children: [
|
|
353
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("h1", {
|
|
354
|
+
style: {
|
|
355
|
+
fontSize: 22,
|
|
356
|
+
margin: 0
|
|
357
|
+
},
|
|
358
|
+
children: t("panel.title")
|
|
359
|
+
}),
|
|
360
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
361
|
+
style: {
|
|
362
|
+
margin: "6px 0 18px",
|
|
363
|
+
color: "var(--dsw-alias-label-secondary)"
|
|
364
|
+
},
|
|
365
|
+
children: t("panel.body")
|
|
366
|
+
}),
|
|
367
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("form", {
|
|
368
|
+
onSubmit: add,
|
|
369
|
+
style: {
|
|
370
|
+
display: "flex",
|
|
371
|
+
gap: 8,
|
|
372
|
+
alignItems: "flex-start"
|
|
373
|
+
},
|
|
374
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("textarea", {
|
|
375
|
+
"data-testid": "note-input",
|
|
376
|
+
value: text,
|
|
377
|
+
onChange: (event) => setText(event.target.value),
|
|
378
|
+
placeholder: t("panel.placeholder"),
|
|
379
|
+
rows: 3,
|
|
380
|
+
style: {
|
|
381
|
+
flex: 1,
|
|
382
|
+
resize: "vertical",
|
|
383
|
+
padding: 10,
|
|
384
|
+
borderRadius: 8,
|
|
385
|
+
border: "1px solid var(--dsw-alias-line, #d9dce1)"
|
|
386
|
+
}
|
|
387
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
388
|
+
"data-testid": "note-add",
|
|
389
|
+
type: "submit",
|
|
390
|
+
disabled: text.trim() === "",
|
|
391
|
+
style: {
|
|
392
|
+
padding: "9px 14px",
|
|
393
|
+
borderRadius: 8
|
|
394
|
+
},
|
|
395
|
+
children: t("panel.add")
|
|
396
|
+
})]
|
|
397
|
+
}),
|
|
398
|
+
error === "" ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
399
|
+
role: "alert",
|
|
400
|
+
style: { color: "var(--dsw-alias-danger, #c0392b)" },
|
|
401
|
+
children: error
|
|
402
|
+
}),
|
|
403
|
+
notes.length === 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
404
|
+
style: {
|
|
405
|
+
marginTop: 24,
|
|
406
|
+
color: "var(--dsw-alias-label-secondary)"
|
|
407
|
+
},
|
|
408
|
+
children: t("panel.empty")
|
|
409
|
+
}) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("ul", {
|
|
410
|
+
style: {
|
|
411
|
+
listStyle: "none",
|
|
412
|
+
padding: 0,
|
|
413
|
+
margin: "24px 0 0",
|
|
414
|
+
display: "grid",
|
|
415
|
+
gap: 8
|
|
416
|
+
},
|
|
417
|
+
children: notes.map((note) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("li", {
|
|
418
|
+
"data-testid": "note-item",
|
|
419
|
+
style: {
|
|
420
|
+
display: "flex",
|
|
421
|
+
gap: 8,
|
|
422
|
+
alignItems: "center",
|
|
423
|
+
padding: 12,
|
|
424
|
+
border: "1px solid var(--dsw-alias-line, #d9dce1)",
|
|
425
|
+
borderRadius: 10
|
|
426
|
+
},
|
|
427
|
+
children: [
|
|
428
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
429
|
+
style: {
|
|
430
|
+
flex: 1,
|
|
431
|
+
textDecoration: note.done ? "line-through" : "none",
|
|
432
|
+
opacity: note.done ? .6 : 1
|
|
433
|
+
},
|
|
434
|
+
children: note.text
|
|
435
|
+
}),
|
|
436
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
437
|
+
type: "button",
|
|
438
|
+
onClick: () => void toggle(note),
|
|
439
|
+
style: {
|
|
440
|
+
padding: "5px 9px",
|
|
441
|
+
borderRadius: 7
|
|
442
|
+
},
|
|
443
|
+
children: note.done ? t("panel.reopen") : t("panel.done")
|
|
444
|
+
}),
|
|
445
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
446
|
+
type: "button",
|
|
447
|
+
onClick: () => void remove(note),
|
|
448
|
+
style: {
|
|
449
|
+
padding: "5px 9px",
|
|
450
|
+
borderRadius: 7
|
|
451
|
+
},
|
|
452
|
+
children: t("panel.delete")
|
|
453
|
+
})
|
|
454
|
+
]
|
|
455
|
+
}, note.id))
|
|
456
|
+
})
|
|
457
|
+
]
|
|
458
|
+
});
|
|
459
|
+
}
|
|
460
|
+
const name = NAME;
|
|
461
|
+
const inject = ["slots", "locale"];
|
|
462
|
+
function apply(ctx) {
|
|
463
|
+
setClientContext(ctx);
|
|
464
|
+
ctx.effect(() => ctx.locale.register(NS, {
|
|
465
|
+
en: {
|
|
466
|
+
"panel.title": "Quick Note",
|
|
467
|
+
"panel.body": "Capture a note, mark it done, keep the list on one page.",
|
|
468
|
+
"panel.placeholder": "Write a note…",
|
|
469
|
+
"panel.add": "Add note",
|
|
470
|
+
"panel.empty": "No notes yet.",
|
|
471
|
+
"panel.done": "Done",
|
|
472
|
+
"panel.reopen": "Reopen",
|
|
473
|
+
"panel.delete": "Delete",
|
|
474
|
+
"panel.deleted": "Note deleted.",
|
|
475
|
+
"panel.completed": "Note updated.",
|
|
476
|
+
"panel.undo": "Undo",
|
|
477
|
+
"toolbar.capture": "Note",
|
|
478
|
+
"overlay.title": "Save a quick note",
|
|
479
|
+
"overlay.cancel": "Cancel",
|
|
480
|
+
"overlay.save": "Save note",
|
|
481
|
+
"overlay.saveClear": "Save and clear draft",
|
|
482
|
+
"overlay.empty": "Nothing to save."
|
|
483
|
+
},
|
|
484
|
+
zh: {
|
|
485
|
+
"panel.title": "随手记",
|
|
486
|
+
"panel.body": "记下一条笔记,标记完成,在一页里管理。",
|
|
487
|
+
"panel.placeholder": "写一条笔记…",
|
|
488
|
+
"panel.add": "添加",
|
|
489
|
+
"panel.empty": "还没有笔记。",
|
|
490
|
+
"panel.done": "完成",
|
|
491
|
+
"panel.reopen": "重新打开",
|
|
492
|
+
"panel.delete": "删除",
|
|
493
|
+
"panel.deleted": "笔记已删除。",
|
|
494
|
+
"panel.completed": "笔记已更新。",
|
|
495
|
+
"panel.undo": "撤销",
|
|
496
|
+
"toolbar.capture": "记一下",
|
|
497
|
+
"overlay.title": "保存随手记",
|
|
498
|
+
"overlay.cancel": "取消",
|
|
499
|
+
"overlay.save": "保存笔记",
|
|
500
|
+
"overlay.saveClear": "保存并清空输入框",
|
|
501
|
+
"overlay.empty": "没有可保存的内容。"
|
|
502
|
+
}
|
|
503
|
+
}), `${NAME}: dictionaries`);
|
|
504
|
+
ctx.slots.inject("sidebar.panellist", () => ctx.slots.register({
|
|
505
|
+
name: "sidebar.panellist",
|
|
506
|
+
id: NAME,
|
|
507
|
+
order: 60,
|
|
508
|
+
label: () => ctx.locale.bind(NS)("panel.title"),
|
|
509
|
+
locale: NS
|
|
510
|
+
}, Icon));
|
|
511
|
+
ctx.slots.inject("main", () => ctx.slots.register({
|
|
512
|
+
name: "main",
|
|
513
|
+
key: NAME,
|
|
514
|
+
locale: NS
|
|
515
|
+
}, Page));
|
|
516
|
+
ctx.slots.inject("conversation.input.left", () => ctx.slots.register({
|
|
517
|
+
name: "conversation.input.left",
|
|
518
|
+
id: "quicknote.toolbar",
|
|
519
|
+
order: 10,
|
|
520
|
+
locale: NS
|
|
521
|
+
}, ComposerButton));
|
|
522
|
+
ctx.slots.inject("shell.overlay", () => ctx.slots.register({
|
|
523
|
+
name: "shell.overlay",
|
|
524
|
+
id: "quicknote.overlay",
|
|
525
|
+
order: 40,
|
|
526
|
+
locale: NS
|
|
527
|
+
}, Overlay));
|
|
528
|
+
}
|
|
529
|
+
exports.apply = apply;
|
|
530
|
+
exports.inject = inject;
|
|
531
|
+
exports.name = name;
|
|
532
|
+
return module.exports;
|
|
533
|
+
}
|
|
534
|
+
});
|
package/lib/index.js
ADDED
|
@@ -0,0 +1,275 @@
|
|
|
1
|
+
import { homedir } from "node:os";
|
|
2
|
+
import { dirname, join } from "node:path";
|
|
3
|
+
import { randomUUID } from "node:crypto";
|
|
4
|
+
import { mkdir, readFile, rename, writeFile } from "node:fs/promises";
|
|
5
|
+
// src/shared/constants.ts
|
|
6
|
+
/** Package name; must equal package.json `name` (the client bundle id). */
|
|
7
|
+
const NAME = "dsh-quicknote";
|
|
8
|
+
/** Host JSON API used by the page. */
|
|
9
|
+
const API = "/api/quicknote";
|
|
10
|
+
// src/host/paths.ts
|
|
11
|
+
/** Keep the example's data separate from other plugins. */
|
|
12
|
+
function resolveNotesFile(ctx) {
|
|
13
|
+
try {
|
|
14
|
+
const dshHomePath = typeof ctx.get === "function" ? ctx.get("dshHomePath") : void 0;
|
|
15
|
+
if (typeof dshHomePath === "function") {
|
|
16
|
+
const dir = dshHomePath("quicknote");
|
|
17
|
+
if (typeof dir === "string" && dir !== "") return join(dir, "notes.json");
|
|
18
|
+
}
|
|
19
|
+
} catch {}
|
|
20
|
+
const home = (process.env.DSH_HOME ?? "").trim() || join(homedir(), ".dsh");
|
|
21
|
+
return join(home, "quicknote", "notes.json");
|
|
22
|
+
}
|
|
23
|
+
// src/host/routes.ts
|
|
24
|
+
var HttpError = class extends Error {
|
|
25
|
+
status;
|
|
26
|
+
constructor(status, message) {
|
|
27
|
+
super(message);
|
|
28
|
+
this.status = status;
|
|
29
|
+
}
|
|
30
|
+
};
|
|
31
|
+
function json(status, body) {
|
|
32
|
+
return new Response(`${JSON.stringify(body)}\n`, {
|
|
33
|
+
status,
|
|
34
|
+
headers: {
|
|
35
|
+
"content-type": "application/json; charset=utf-8",
|
|
36
|
+
"cache-control": "no-store"
|
|
37
|
+
}
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
async function body(request) {
|
|
41
|
+
try {
|
|
42
|
+
const value = await request.json();
|
|
43
|
+
return value && typeof value === "object" ? value : {};
|
|
44
|
+
} catch {
|
|
45
|
+
throw new HttpError(400, "invalid-json");
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
function registerRoutes(ctx, store) {
|
|
49
|
+
ctx.inject(["connection"], (scope) => {
|
|
50
|
+
const registry = scope.get("connection")?.fetch;
|
|
51
|
+
if (typeof registry?.register !== "function") {
|
|
52
|
+
ctx.logger?.warn?.("dsh-quicknote: no fetch registry; the page will not load notes");
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
const wrap = (handler) => async (request) => {
|
|
56
|
+
try {
|
|
57
|
+
return await handler(request);
|
|
58
|
+
} catch (error) {
|
|
59
|
+
return json(error instanceof HttpError ? error.status : 500, {
|
|
60
|
+
ok: false,
|
|
61
|
+
error: error instanceof HttpError ? error.message : String(error?.message ?? error)
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
};
|
|
65
|
+
const routes = [
|
|
66
|
+
{
|
|
67
|
+
path: `${API}/notes`,
|
|
68
|
+
methods: ["GET"],
|
|
69
|
+
fetch: wrap(async () => json(200, {
|
|
70
|
+
ok: true,
|
|
71
|
+
notes: await store.list()
|
|
72
|
+
}))
|
|
73
|
+
},
|
|
74
|
+
{
|
|
75
|
+
path: `${API}/notes/create`,
|
|
76
|
+
methods: ["POST"],
|
|
77
|
+
fetch: wrap(async (request) => json(200, {
|
|
78
|
+
ok: true,
|
|
79
|
+
note: await store.add((await body(request)).text)
|
|
80
|
+
}))
|
|
81
|
+
},
|
|
82
|
+
{
|
|
83
|
+
path: `${API}/notes/update`,
|
|
84
|
+
methods: ["POST"],
|
|
85
|
+
fetch: wrap(async (request) => {
|
|
86
|
+
const input = await body(request);
|
|
87
|
+
const note = await store.update(input.id, input.done === true);
|
|
88
|
+
return note ? json(200, {
|
|
89
|
+
ok: true,
|
|
90
|
+
note
|
|
91
|
+
}) : json(404, {
|
|
92
|
+
ok: false,
|
|
93
|
+
error: "unknown-note"
|
|
94
|
+
});
|
|
95
|
+
})
|
|
96
|
+
},
|
|
97
|
+
{
|
|
98
|
+
path: `${API}/notes/delete`,
|
|
99
|
+
methods: ["POST"],
|
|
100
|
+
fetch: wrap(async (request) => json(200, {
|
|
101
|
+
ok: true,
|
|
102
|
+
removed: await store.remove((await body(request)).id)
|
|
103
|
+
}))
|
|
104
|
+
}
|
|
105
|
+
];
|
|
106
|
+
for (const route of routes) scope.effect(() => registry.register({
|
|
107
|
+
path: route.path,
|
|
108
|
+
methods: route.methods,
|
|
109
|
+
requestBody: "buffered",
|
|
110
|
+
fetch: route.fetch
|
|
111
|
+
}), `dsh-quicknote: ${route.path}`);
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
// src/host/store.ts
|
|
115
|
+
const MAX_TEXT = 2e4;
|
|
116
|
+
/** A small durable store: one JSON file, atomic whole-file writes, newest first. */
|
|
117
|
+
function createStore(file) {
|
|
118
|
+
let cache = null;
|
|
119
|
+
async function load() {
|
|
120
|
+
if (cache) return cache;
|
|
121
|
+
try {
|
|
122
|
+
const parsed = JSON.parse(await readFile(file, "utf8"));
|
|
123
|
+
cache = Array.isArray(parsed.notes) ? parsed.notes.filter((note) => typeof note === "object" && note !== null && typeof note.id === "string" && typeof note.text === "string" && note.text.trim() !== "") : [];
|
|
124
|
+
} catch {
|
|
125
|
+
cache = [];
|
|
126
|
+
}
|
|
127
|
+
return cache;
|
|
128
|
+
}
|
|
129
|
+
async function save(notes) {
|
|
130
|
+
await mkdir(dirname(file), { recursive: true });
|
|
131
|
+
const temporary = `${file}.${process.pid}.tmp`;
|
|
132
|
+
await writeFile(temporary, `${JSON.stringify({ notes }, null, 2)}\n`, { mode: 384 });
|
|
133
|
+
await rename(temporary, file);
|
|
134
|
+
cache = notes;
|
|
135
|
+
}
|
|
136
|
+
return {
|
|
137
|
+
file,
|
|
138
|
+
async list() {
|
|
139
|
+
return [...await load()];
|
|
140
|
+
},
|
|
141
|
+
async add(text) {
|
|
142
|
+
const value = String(text ?? "").trim();
|
|
143
|
+
if (value === "") throw new Error("empty-note");
|
|
144
|
+
if (value.length > MAX_TEXT) throw new Error("note-too-long");
|
|
145
|
+
const note = {
|
|
146
|
+
id: randomUUID(),
|
|
147
|
+
text: value,
|
|
148
|
+
done: false,
|
|
149
|
+
createdAt: Date.now()
|
|
150
|
+
};
|
|
151
|
+
await save([note, ...await load()]);
|
|
152
|
+
return note;
|
|
153
|
+
},
|
|
154
|
+
async update(id, done) {
|
|
155
|
+
const notes = await load();
|
|
156
|
+
const current = notes.find((note) => note.id === id);
|
|
157
|
+
if (!current) return null;
|
|
158
|
+
const updated = {
|
|
159
|
+
...current,
|
|
160
|
+
done
|
|
161
|
+
};
|
|
162
|
+
await save(notes.map((note) => note.id === id ? updated : note));
|
|
163
|
+
return updated;
|
|
164
|
+
},
|
|
165
|
+
async remove(id) {
|
|
166
|
+
const notes = await load();
|
|
167
|
+
const next = notes.filter((note) => note.id !== id);
|
|
168
|
+
if (next.length === notes.length) return false;
|
|
169
|
+
await save(next);
|
|
170
|
+
return true;
|
|
171
|
+
}
|
|
172
|
+
};
|
|
173
|
+
}
|
|
174
|
+
// src/host/tools.ts
|
|
175
|
+
function textOutput() {
|
|
176
|
+
return {
|
|
177
|
+
schema: { type: "string" },
|
|
178
|
+
render: (_args, value) => [{
|
|
179
|
+
type: "text",
|
|
180
|
+
text: String(value)
|
|
181
|
+
}]
|
|
182
|
+
};
|
|
183
|
+
}
|
|
184
|
+
function describe(note) {
|
|
185
|
+
const text = note.text.length > 200 ? `${note.text.slice(0, 200)}…` : note.text;
|
|
186
|
+
return `- ${note.id} (${note.done ? "done" : "open"}): ${text}`;
|
|
187
|
+
}
|
|
188
|
+
function createTools(store) {
|
|
189
|
+
return [
|
|
190
|
+
{
|
|
191
|
+
name: "quicknote_add",
|
|
192
|
+
description: "Save one note in the user's Quick Note list. Use it when the user asks to remember or park something for later.",
|
|
193
|
+
parameters: {
|
|
194
|
+
type: "object",
|
|
195
|
+
additionalProperties: false,
|
|
196
|
+
required: ["text"],
|
|
197
|
+
properties: { text: {
|
|
198
|
+
type: "string",
|
|
199
|
+
description: "The note body."
|
|
200
|
+
} }
|
|
201
|
+
},
|
|
202
|
+
output: textOutput(),
|
|
203
|
+
async execute(args) {
|
|
204
|
+
const note = await store.add(args?.text);
|
|
205
|
+
return `Saved as ${note.id}.\n${describe(note)}`;
|
|
206
|
+
}
|
|
207
|
+
},
|
|
208
|
+
{
|
|
209
|
+
name: "quicknote_list",
|
|
210
|
+
description: "List notes in the user's Quick Note list, newest first.",
|
|
211
|
+
parameters: {
|
|
212
|
+
type: "object",
|
|
213
|
+
additionalProperties: false,
|
|
214
|
+
properties: {}
|
|
215
|
+
},
|
|
216
|
+
output: textOutput(),
|
|
217
|
+
async execute() {
|
|
218
|
+
const notes = await store.list();
|
|
219
|
+
return notes.length === 0 ? "The Quick Note list is empty." : `${notes.length} note${notes.length === 1 ? "" : "s"}:\n${notes.map(describe).join("\n")}`;
|
|
220
|
+
}
|
|
221
|
+
},
|
|
222
|
+
{
|
|
223
|
+
name: "quicknote_done",
|
|
224
|
+
description: "Mark one Quick Note as done, or reopen it with done=false.",
|
|
225
|
+
parameters: {
|
|
226
|
+
type: "object",
|
|
227
|
+
additionalProperties: false,
|
|
228
|
+
required: ["id"],
|
|
229
|
+
properties: {
|
|
230
|
+
id: { type: "string" },
|
|
231
|
+
done: { type: "boolean" }
|
|
232
|
+
}
|
|
233
|
+
},
|
|
234
|
+
output: textOutput(),
|
|
235
|
+
async execute(args) {
|
|
236
|
+
const note = await store.update(args?.id, args?.done === false ? false : true);
|
|
237
|
+
return note ? `Note ${note.id} is ${note.done ? "done" : "open"}.` : `No note with id "${args?.id}".`;
|
|
238
|
+
}
|
|
239
|
+
},
|
|
240
|
+
{
|
|
241
|
+
name: "quicknote_delete",
|
|
242
|
+
description: "Delete one Quick Note permanently. Only when the user explicitly asks.",
|
|
243
|
+
parameters: {
|
|
244
|
+
type: "object",
|
|
245
|
+
additionalProperties: false,
|
|
246
|
+
required: ["id"],
|
|
247
|
+
properties: { id: { type: "string" } }
|
|
248
|
+
},
|
|
249
|
+
output: textOutput(),
|
|
250
|
+
async execute(args) {
|
|
251
|
+
return await store.remove(args?.id) ? `Deleted ${args?.id}.` : `No note with id "${args?.id}".`;
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
];
|
|
255
|
+
}
|
|
256
|
+
// src/host/index.ts
|
|
257
|
+
/**
|
|
258
|
+
* Host half of the Quick Note example: one JSON file, four routes, four tools.
|
|
259
|
+
*
|
|
260
|
+
* @module dsh-quicknote/host
|
|
261
|
+
*/
|
|
262
|
+
const name = NAME;
|
|
263
|
+
function apply(ctx) {
|
|
264
|
+
const store = createStore(resolveNotesFile(ctx));
|
|
265
|
+
ctx.effect(() => {
|
|
266
|
+
store.list().catch((error) => {
|
|
267
|
+
ctx.logger?.warn?.(`${NAME}: could not read notes: ${String(error)}`);
|
|
268
|
+
});
|
|
269
|
+
}, `${NAME}: warm the note store`);
|
|
270
|
+
registerRoutes(ctx, store);
|
|
271
|
+
ctx.inject(["tools"], (scope) => {
|
|
272
|
+
for (const definition of createTools(store)) scope.effect(() => scope.tools.register(definition), `${NAME}: tool ${definition.name}`);
|
|
273
|
+
});
|
|
274
|
+
}
|
|
275
|
+
export { apply, createStore, name, resolveNotesFile };
|
package/llms.txt
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# dsh-quicknote
|
|
2
|
+
|
|
3
|
+
> Capture a note from the DSH composer, keep it out of the way, and manage the list on one page.
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
dsh plugin --profile <profile> add dsh-quicknote
|
|
8
|
+
|
|
9
|
+
## Verify
|
|
10
|
+
|
|
11
|
+
dsh --profile <profile> --dump-config | grep -A2 "id: dsh-quicknote"
|
|
12
|
+
|
|
13
|
+
## Develop
|
|
14
|
+
|
|
15
|
+
pnpm install
|
|
16
|
+
pnpm verify
|
|
17
|
+
|
|
18
|
+
## Read next
|
|
19
|
+
|
|
20
|
+
- README.md — user guide
|
|
21
|
+
- AGENTS.md — repository rules and invariants
|
|
22
|
+
- src/host/ — Host half
|
|
23
|
+
- src/client/ — browser half
|
|
24
|
+
- tests/ — both suites
|
|
25
|
+
|
|
26
|
+
## Contracts
|
|
27
|
+
|
|
28
|
+
- Package: dsh-quicknote
|
|
29
|
+
- Patch row: `dsh-quicknote`
|
|
30
|
+
- Client module id: `dsh-quicknote`
|
package/locale/en.json
ADDED
package/locale/zh.json
ADDED
package/package.json
CHANGED
|
@@ -1,6 +1,78 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-quicknote",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Capture a note from the DSH composer, keep it out of the way, and manage the list on one page.",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"author": "dsh-kit contributors",
|
|
7
|
+
"type": "module",
|
|
8
|
+
"icon": "./icon.svg",
|
|
9
|
+
"exports": {
|
|
10
|
+
".": "./lib/index.js",
|
|
11
|
+
"./client": "./lib/client.js",
|
|
12
|
+
"./package.json": "./package.json"
|
|
13
|
+
},
|
|
14
|
+
"files": [
|
|
15
|
+
"lib/",
|
|
16
|
+
"cordis.patch.yml",
|
|
17
|
+
"icon.svg",
|
|
18
|
+
"locale/*.json",
|
|
19
|
+
"README.md",
|
|
20
|
+
"README.zh.md",
|
|
21
|
+
"AGENTS.md",
|
|
22
|
+
"llms.txt",
|
|
23
|
+
"LICENSE",
|
|
24
|
+
"CHANGELOG.md",
|
|
25
|
+
"PUBLISHING.md"
|
|
26
|
+
],
|
|
27
|
+
"scripts": {
|
|
28
|
+
"build": "tsdown",
|
|
29
|
+
"typecheck": "tsc --noEmit",
|
|
30
|
+
"lint": "oxlint src tests scripts",
|
|
31
|
+
"format": "oxfmt --check src tests scripts",
|
|
32
|
+
"format:write": "oxfmt src tests scripts",
|
|
33
|
+
"test": "vitest run",
|
|
34
|
+
"verify": "oxfmt --check src tests scripts && oxlint src tests scripts && tsc --noEmit && vitest run && tsdown && node scripts/check.mjs && npm pack --dry-run",
|
|
35
|
+
"prepack": "tsdown"
|
|
36
|
+
},
|
|
37
|
+
"engines": {
|
|
38
|
+
"node": ">=24"
|
|
39
|
+
},
|
|
40
|
+
"dsh": {
|
|
41
|
+
"manifestVersion": 1,
|
|
42
|
+
"bundle": {
|
|
43
|
+
"patch": "./cordis.patch.yml"
|
|
44
|
+
},
|
|
45
|
+
"client": {
|
|
46
|
+
"platform": "web"
|
|
47
|
+
},
|
|
48
|
+
"engines": {
|
|
49
|
+
"dsh": ">=0.2.1-rc.5"
|
|
50
|
+
}
|
|
51
|
+
},
|
|
52
|
+
"devDependencies": {
|
|
53
|
+
"@types/node": "^26.6.6",
|
|
54
|
+
"@types/react": "^19.3.0",
|
|
55
|
+
"jsdom": "^30.1.3",
|
|
56
|
+
"oxfmt": "^0.75.0",
|
|
57
|
+
"oxlint": "^1.90.0",
|
|
58
|
+
"react": "^19.3.1",
|
|
59
|
+
"tsdown": "^0.23.4",
|
|
60
|
+
"typescript": "^7.0.2",
|
|
61
|
+
"vitest": "^5.0.5"
|
|
62
|
+
},
|
|
63
|
+
"keywords": [
|
|
64
|
+
"dsh",
|
|
65
|
+
"dsh-plugin",
|
|
66
|
+
"deepseek-harness",
|
|
67
|
+
"quicknote",
|
|
68
|
+
"notes"
|
|
69
|
+
],
|
|
70
|
+
"repository": {
|
|
71
|
+
"type": "git",
|
|
72
|
+
"url": "git+https://github.com/dsh-kit/dsh-quicknote.git"
|
|
73
|
+
},
|
|
74
|
+
"homepage": "https://github.com/dsh-kit/dsh-quicknote",
|
|
75
|
+
"bugs": {
|
|
76
|
+
"url": "https://github.com/dsh-kit/dsh-quicknote/issues"
|
|
77
|
+
}
|
|
78
|
+
}
|