dsh-tiddlywiki 0.1.2 → 0.2.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 +7 -4
- package/lib/client.bundle.js +208 -18
- package/lib/client.js +208 -18
- package/lib/index.js +2263 -2152
- package/lib/index.js.map +1 -1
- package/package.json +1 -1
- package/src/client/note-widget.ts +373 -210
- package/src/client/settings-page.ts +3 -0
- package/src/client/styles.ts +43 -0
- package/src/host/config.ts +6 -0
- package/src/host/routes.ts +49 -10
- package/src/host/seed-notes.ts +74 -14
- package/src/index.ts +22 -7
package/README.md
CHANGED
|
@@ -24,7 +24,7 @@ TiddlyWiki 的特性正好合适:单文件、纯文本、wiki 语法、自带
|
|
|
24
24
|
|---|---|
|
|
25
25
|
| Agent 工具 | `tiddlywiki_search` / `get` / `put` / `delete` / `git_sync` |
|
|
26
26
|
| 人编辑 | 侧边栏「TiddlyWiki」入口 → 中央列内嵌完整 TW 编辑器(iframe 直连 TW 服务) |
|
|
27
|
-
| 快速笔记 |
|
|
27
|
+
| 快速笔记 | 聊天区右下角常驻悬浮控件(可折叠,可多选/自动补全 tag,Ctrl+Enter 保存;设置页可整体隐藏) |
|
|
28
28
|
| 零摩擦生命周期 | 随 dsh 自动启动/关闭;TW 子进程崩溃自动重启(退避);端口/目录/首次 git init 全自动 |
|
|
29
29
|
| 同步 | 单线程交替模型:开工 pull,收工 commit+push,自动 commit(60s 防抖,可关) |
|
|
30
30
|
|
|
@@ -43,13 +43,13 @@ dsh plugin --profile web add github:bbqisbbq/dsh-tiddlywiki
|
|
|
43
43
|
dsh plugin --profile web add link:C:\Users\bbq\.dsh\plugins\dsh-tiddlywiki
|
|
44
44
|
```
|
|
45
45
|
|
|
46
|
-
首次启动会向 wiki
|
|
46
|
+
首次启动会向 wiki **写入一次**「dsh-tiddlywiki 插件说明」笔记(tag `docs`),并默认安装「侧边移到左侧.css」界面补丁(把 TW 右侧栏移到左边;被删除后,只要设置页「侧边栏移左 CSS」开关开着就会随启动补丁还原)。
|
|
47
47
|
|
|
48
48
|
## 发布 / 仓库
|
|
49
49
|
|
|
50
50
|
- **GitHub(公开)**:https://github.com/bbqisbbq/dsh-tiddlywiki
|
|
51
51
|
- **npm**:`dsh-tiddlywiki`(`npm i dsh-tiddlywiki`;https://www.npmjs.com/package/dsh-tiddlywiki )
|
|
52
|
-
-
|
|
52
|
+
- **说明笔记**:插件在**首次启动**时向 wiki **写入一次**「`dsh-tiddlywiki 插件说明`」笔记(tag `docs`)——只写一次、手动编辑不被覆盖、**删除后重启不会自动恢复**(一次性标记;清空 wiki 重装会再写入)。当前 live wiki 里已有一份。
|
|
53
53
|
|
|
54
54
|
## 可被检索的标准字段
|
|
55
55
|
|
|
@@ -85,6 +85,9 @@ dsh plugin --profile web add link:C:\Users\bbq\.dsh\plugins\dsh-tiddlywiki
|
|
|
85
85
|
branch: "main"
|
|
86
86
|
note:
|
|
87
87
|
tag: "inbox"
|
|
88
|
+
ui:
|
|
89
|
+
showQuickNote: true # 是否显示右下角「快速笔记」按钮
|
|
90
|
+
sidebarLeftCss: true # 是否启用「侧边移到左侧.css」补丁(被删后随启动还原)
|
|
88
91
|
auth:
|
|
89
92
|
username: "" # 默认 loopback 匿名;暴露到非 loopback 时才需要
|
|
90
93
|
password: ""
|
|
@@ -95,7 +98,7 @@ dsh plugin --profile web add link:C:\Users\bbq\.dsh\plugins\dsh-tiddlywiki
|
|
|
95
98
|
|
|
96
99
|
> 运行时配置:设置页(见「设置页」)写入的 `$:/plugins/dsh-tiddlywiki/config`
|
|
97
100
|
> tiddler 是 `config:` 块之上的覆盖层(tiddler 优先、随 wiki 的 git 同步)。
|
|
98
|
-
> 无需改动 cordis 也能改 note tag / git 开关等。
|
|
101
|
+
> 无需改动 cordis 也能改 note tag / git 开关 / ui 开关等。
|
|
99
102
|
|
|
100
103
|
## 使用
|
|
101
104
|
|
package/lib/client.bundle.js
CHANGED
|
@@ -48,6 +48,21 @@ const CSS_TEXT = `
|
|
|
48
48
|
.dsh-tw-entry[data-active="true"] { background: var(--dsw-alias-interactive-bg-active, rgba(128,128,128,.18)); color: var(--dsw-alias-label-primary, inherit); font-weight: 500; }
|
|
49
49
|
.dsh-tw-entry svg { flex: none; }
|
|
50
50
|
.dsh-tw-entry .dsh-tw-entry-label { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
51
|
+
/* Collapsed rail: the shell narrows the sidebar to a 56px icon rail (dual
|
|
52
|
+
signals — the frame's data-sidebar-collapsed + the sidebar root's hashed
|
|
53
|
+
*_collapsed class, same doctrine as dsh-taskboard 0.4.3). Mirror the
|
|
54
|
+
native 36×36 icon-button geometry: center the icon, hide the label, and
|
|
55
|
+
scale it up to match the shell's 18px rail icons. */
|
|
56
|
+
[data-sidebar-collapsed] [data-dsh-tw-entry],
|
|
57
|
+
[class*="_collapsed"] [data-dsh-tw-entry] {
|
|
58
|
+
width: 36px; height: 36px; min-width: 36px;
|
|
59
|
+
margin: 0 0 12px; padding: 0;
|
|
60
|
+
justify-content: center; gap: 0; text-align: center;
|
|
61
|
+
}
|
|
62
|
+
[data-sidebar-collapsed] [data-dsh-tw-entry] .dsh-tw-entry-label,
|
|
63
|
+
[class*="_collapsed"] [data-dsh-tw-entry] .dsh-tw-entry-label { display: none; }
|
|
64
|
+
[data-sidebar-collapsed] [data-dsh-tw-entry] svg,
|
|
65
|
+
[class*="_collapsed"] [data-dsh-tw-entry] svg { width: 18px; height: 18px; }
|
|
51
66
|
|
|
52
67
|
/* ── center-column panel (fixed overlay, JS-pinned to the column rect) ── */
|
|
53
68
|
.dsh-tw-view {
|
|
@@ -163,6 +178,34 @@ html[data-dsh-tw-active] .dshDesktopConversationSurface > :not([data-dsh-tw-view
|
|
|
163
178
|
}
|
|
164
179
|
.dsh-tw-note-edit:hover:not(:disabled) { filter: brightness(.96); }
|
|
165
180
|
.dsh-tw-note-edit:disabled { opacity: .55; cursor: default; }
|
|
181
|
+
/* ── Multi-tag chip editor (quick-note tags) ─────────────────────────────── */
|
|
182
|
+
.dsh-tw-note-tags { position: relative; flex: 1; min-width: 0; display: flex; flex-direction: column; gap: 4px; }
|
|
183
|
+
.dsh-tw-note-chips { display: flex; flex-wrap: wrap; gap: 4px; }
|
|
184
|
+
.dsh-tw-note-tagchip {
|
|
185
|
+
display: inline-flex; align-items: center; gap: 4px;
|
|
186
|
+
padding: 2px 6px 2px 8px; border-radius: 999px; font-size: 11px; line-height: 1.4;
|
|
187
|
+
background: color-mix(in srgb, var(--dsw-alias-brand-primary, #3e63dd) 10%, var(--dsw-alias-bg-layer-1, #fff));
|
|
188
|
+
border: 1px solid color-mix(in srgb, var(--dsw-alias-brand-primary, #3e63dd) 30%, transparent);
|
|
189
|
+
color: var(--dsw-alias-label-primary, #222);
|
|
190
|
+
}
|
|
191
|
+
.dsh-tw-note-tagchip-x {
|
|
192
|
+
cursor: pointer; font-size: 12px; line-height: 1; padding: 0 2px; border-radius: 50%;
|
|
193
|
+
color: var(--dsw-alias-label-secondary, #888);
|
|
194
|
+
}
|
|
195
|
+
.dsh-tw-note-tagchip-x:hover { color: var(--dsw-alias-state-error-primary, #d13b3b); }
|
|
196
|
+
.dsh-tw-note-taginput { width: 100%; box-sizing: border-box; }
|
|
197
|
+
.dsh-tw-note-tagsuggest {
|
|
198
|
+
position: absolute; left: 0; right: 0; top: calc(100% + 2px); z-index: 60;
|
|
199
|
+
max-height: 168px; overflow-y: auto; border-radius: 8px; padding: 4px;
|
|
200
|
+
background: var(--dsw-alias-bg-layer-2, #fff); color: var(--dsw-alias-label-primary, #222);
|
|
201
|
+
border: 1px solid var(--dsw-alias-border-l2, rgba(0,0,0,.18));
|
|
202
|
+
box-shadow: var(--dsw-shadow-lv2, 0 4px 16px rgba(0,0,0,.16));
|
|
203
|
+
font-size: 12px;
|
|
204
|
+
}
|
|
205
|
+
.dsh-tw-note-tagsuggest-item {
|
|
206
|
+
padding: 6px 9px; border-radius: 6px; cursor: pointer; white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
|
|
207
|
+
}
|
|
208
|
+
.dsh-tw-note-tagsuggest-item:hover { background: var(--dsw-alias-interactive-bg-hover, rgba(128,128,128,.12)); }
|
|
166
209
|
/* ── Native-editor popup iframe (quick-note "在 TW 中编辑") ──────────────── */
|
|
167
210
|
.dsh-tw-editor-popup {
|
|
168
211
|
position: fixed; left: 0; right: 0; top: 0; bottom: 0; margin: auto;
|
|
@@ -847,14 +890,23 @@ function ensurePopup() {
|
|
|
847
890
|
* collapsible, independent of any shell DOM. Lets the human jot drafts /
|
|
848
891
|
* scratch notes while waiting for the AI or drafting the next prompt.
|
|
849
892
|
*
|
|
850
|
-
* Save posts to /dsh-tiddlywiki/note → an independent tiddler (title &
|
|
893
|
+
* Save posts to /dsh-tiddlywiki/note → an independent tiddler (title & tags
|
|
851
894
|
* editable; defaults: timestamp title + config tag, usually "inbox").
|
|
852
895
|
*
|
|
896
|
+
* ui.showQuickNote config: the widget is mounted async and stays hidden (and
|
|
897
|
+
* never appended) when the option is off, so the toggle button can be disabled
|
|
898
|
+
* from the settings page without touching the shell.
|
|
899
|
+
*
|
|
900
|
+
* Tag editor: multi-select chips + autocomplete from the wiki's existing tags
|
|
901
|
+
* (GET /dsh-tiddlywiki/tags). Enter/`,` commits the typed value; Backspace on
|
|
902
|
+
* an empty field removes the last chip; the dropdown filters on typing.
|
|
903
|
+
*
|
|
853
904
|
* @module dsh-tiddlywiki/client/note-widget
|
|
854
905
|
*/
|
|
855
906
|
const NOTE_ENDPOINT = "/dsh-tiddlywiki/note";
|
|
856
907
|
const EDIT_ENDPOINT = "/dsh-tiddlywiki/edit";
|
|
857
908
|
const STATUS_ENDPOINT = "/dsh-tiddlywiki/status";
|
|
909
|
+
const TAGS_ENDPOINT = "/dsh-tiddlywiki/tags";
|
|
858
910
|
function pad(n) {
|
|
859
911
|
return n < 10 ? `0${n}` : String(n);
|
|
860
912
|
}
|
|
@@ -862,16 +914,137 @@ function pad(n) {
|
|
|
862
914
|
function timestampTitle(date = /* @__PURE__ */ new Date()) {
|
|
863
915
|
return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())} ${pad(date.getHours())}:${pad(date.getMinutes())}`;
|
|
864
916
|
}
|
|
865
|
-
async function
|
|
917
|
+
async function fetchUiOptions() {
|
|
918
|
+
const fallback = {
|
|
919
|
+
showQuickNote: true,
|
|
920
|
+
defaultTag: "inbox"
|
|
921
|
+
};
|
|
866
922
|
try {
|
|
867
923
|
const res = await fetch(STATUS_ENDPOINT, { signal: AbortSignal.timeout(5e3) });
|
|
868
|
-
if (!res.ok) return
|
|
869
|
-
|
|
924
|
+
if (!res.ok) return fallback;
|
|
925
|
+
const payload = await res.json();
|
|
926
|
+
return {
|
|
927
|
+
showQuickNote: payload.ui?.showQuickNote !== false,
|
|
928
|
+
defaultTag: payload.note?.tag ?? "inbox"
|
|
929
|
+
};
|
|
870
930
|
} catch {
|
|
871
|
-
return
|
|
931
|
+
return fallback;
|
|
872
932
|
}
|
|
873
933
|
}
|
|
874
|
-
|
|
934
|
+
/** Multi-tag chip editor with autocomplete from the wiki's existing tags. */
|
|
935
|
+
function buildTagEditor() {
|
|
936
|
+
const wrap = document.createElement("div");
|
|
937
|
+
wrap.className = "dsh-tw-note-tags";
|
|
938
|
+
const chipWrap = document.createElement("div");
|
|
939
|
+
chipWrap.className = "dsh-tw-note-chips";
|
|
940
|
+
const input = document.createElement("input");
|
|
941
|
+
input.className = "dsh-tw-note-taginput";
|
|
942
|
+
input.placeholder = "tag(可多选,自动补全)";
|
|
943
|
+
const suggest = document.createElement("div");
|
|
944
|
+
suggest.className = "dsh-tw-note-tagsuggest";
|
|
945
|
+
suggest.hidden = true;
|
|
946
|
+
wrap.append(chipWrap, input, suggest);
|
|
947
|
+
const chips = [];
|
|
948
|
+
const hideSuggest = () => {
|
|
949
|
+
suggest.hidden = true;
|
|
950
|
+
};
|
|
951
|
+
const renderChips = () => {
|
|
952
|
+
chipWrap.replaceChildren();
|
|
953
|
+
for (const tag of chips) {
|
|
954
|
+
const chip = document.createElement("span");
|
|
955
|
+
chip.className = "dsh-tw-note-tagchip";
|
|
956
|
+
chip.textContent = tag;
|
|
957
|
+
const x = document.createElement("span");
|
|
958
|
+
x.className = "dsh-tw-note-tagchip-x";
|
|
959
|
+
x.textContent = "×";
|
|
960
|
+
x.title = `移除 tag「${tag}」`;
|
|
961
|
+
x.addEventListener("click", (event) => {
|
|
962
|
+
event.stopPropagation();
|
|
963
|
+
const i = chips.indexOf(tag);
|
|
964
|
+
if (i >= 0) {
|
|
965
|
+
chips.splice(i, 1);
|
|
966
|
+
renderChips();
|
|
967
|
+
}
|
|
968
|
+
});
|
|
969
|
+
chip.append(x);
|
|
970
|
+
chipWrap.append(chip);
|
|
971
|
+
}
|
|
972
|
+
};
|
|
973
|
+
const addTag = (tag) => {
|
|
974
|
+
const t = tag.trim();
|
|
975
|
+
if (t.length === 0 || chips.includes(t)) return;
|
|
976
|
+
chips.push(t);
|
|
977
|
+
input.value = "";
|
|
978
|
+
renderChips();
|
|
979
|
+
hideSuggest();
|
|
980
|
+
input.focus();
|
|
981
|
+
};
|
|
982
|
+
const commitInput = () => {
|
|
983
|
+
for (const raw of input.value.split(/\s+/)) addTag(raw);
|
|
984
|
+
};
|
|
985
|
+
let knownTags = [];
|
|
986
|
+
let tagsPromise;
|
|
987
|
+
const ensureTags = () => {
|
|
988
|
+
tagsPromise ??= fetch(TAGS_ENDPOINT, { signal: AbortSignal.timeout(5e3) }).then((r) => r.ok ? r.json() : Promise.resolve({})).then((p) => [...p.tags ?? []].sort((a, b) => a.localeCompare(b, "zh"))).catch(() => []);
|
|
989
|
+
tagsPromise.then((list) => {
|
|
990
|
+
knownTags = list;
|
|
991
|
+
});
|
|
992
|
+
return tagsPromise;
|
|
993
|
+
};
|
|
994
|
+
const showSuggest = () => {
|
|
995
|
+
const q = input.value.trim().toLowerCase();
|
|
996
|
+
const matches = knownTags.filter((t) => !chips.includes(t) && (q.length === 0 || t.toLowerCase().includes(q))).slice(0, 8);
|
|
997
|
+
suggest.replaceChildren();
|
|
998
|
+
for (const tag of matches) {
|
|
999
|
+
const item = document.createElement("div");
|
|
1000
|
+
item.className = "dsh-tw-note-tagsuggest-item";
|
|
1001
|
+
item.textContent = tag;
|
|
1002
|
+
item.addEventListener("mousedown", (event) => {
|
|
1003
|
+
event.preventDefault();
|
|
1004
|
+
addTag(tag);
|
|
1005
|
+
});
|
|
1006
|
+
suggest.append(item);
|
|
1007
|
+
}
|
|
1008
|
+
suggest.hidden = matches.length === 0;
|
|
1009
|
+
};
|
|
1010
|
+
input.addEventListener("focus", () => {
|
|
1011
|
+
ensureTags().then(showSuggest);
|
|
1012
|
+
});
|
|
1013
|
+
input.addEventListener("input", () => {
|
|
1014
|
+
if (knownTags.length === 0) ensureTags().then(showSuggest);
|
|
1015
|
+
else showSuggest();
|
|
1016
|
+
});
|
|
1017
|
+
input.addEventListener("keydown", (event) => {
|
|
1018
|
+
if (event.key === "Enter" || event.key === ",") {
|
|
1019
|
+
event.preventDefault();
|
|
1020
|
+
commitInput();
|
|
1021
|
+
} else if (event.key === "Backspace" && input.value.length === 0 && chips.length > 0) {
|
|
1022
|
+
chips.pop();
|
|
1023
|
+
renderChips();
|
|
1024
|
+
} else if (event.key === "Escape") hideSuggest();
|
|
1025
|
+
});
|
|
1026
|
+
document.addEventListener("click", (event) => {
|
|
1027
|
+
if (!wrap.contains(event.target)) hideSuggest();
|
|
1028
|
+
}, true);
|
|
1029
|
+
return {
|
|
1030
|
+
el: wrap,
|
|
1031
|
+
getTags: () => {
|
|
1032
|
+
commitInput();
|
|
1033
|
+
return [...chips];
|
|
1034
|
+
},
|
|
1035
|
+
setDefault: (tag) => {
|
|
1036
|
+
if (chips.length === 0) {
|
|
1037
|
+
const t = tag.trim();
|
|
1038
|
+
if (t.length > 0) {
|
|
1039
|
+
chips.push(t);
|
|
1040
|
+
renderChips();
|
|
1041
|
+
}
|
|
1042
|
+
}
|
|
1043
|
+
}
|
|
1044
|
+
};
|
|
1045
|
+
}
|
|
1046
|
+
/** Build the whole widget DOM and wire it up (returns the appended root). */
|
|
1047
|
+
function buildWidget() {
|
|
875
1048
|
const root = document.createElement("div");
|
|
876
1049
|
root.className = "dsh-tw-note";
|
|
877
1050
|
const card = document.createElement("div");
|
|
@@ -893,10 +1066,8 @@ function mountNoteWidget() {
|
|
|
893
1066
|
const titleInput = document.createElement("input");
|
|
894
1067
|
titleInput.className = "dsh-tw-note-title";
|
|
895
1068
|
titleInput.placeholder = "标题(默认时间戳)";
|
|
896
|
-
const
|
|
897
|
-
|
|
898
|
-
tagInput.placeholder = "tag(默认 inbox)";
|
|
899
|
-
fields.append(titleInput, tagInput);
|
|
1069
|
+
const tagEditor = buildTagEditor();
|
|
1070
|
+
fields.append(titleInput, tagEditor.el);
|
|
900
1071
|
const textarea = document.createElement("textarea");
|
|
901
1072
|
textarea.className = "dsh-tw-note-text";
|
|
902
1073
|
textarea.placeholder = "写点东西…(Ctrl+Enter 保存)";
|
|
@@ -931,10 +1102,9 @@ function mountNoteWidget() {
|
|
|
931
1102
|
card.hidden = false;
|
|
932
1103
|
opened = true;
|
|
933
1104
|
resetTitle();
|
|
934
|
-
if (tagInput.value.length === 0) tagInput.value = defaultTag;
|
|
935
1105
|
textarea.focus();
|
|
936
|
-
defaultTag = await
|
|
937
|
-
|
|
1106
|
+
defaultTag = (await fetchUiOptions()).defaultTag;
|
|
1107
|
+
tagEditor.setDefault(defaultTag);
|
|
938
1108
|
};
|
|
939
1109
|
const close = () => {
|
|
940
1110
|
card.hidden = true;
|
|
@@ -958,7 +1128,7 @@ function mountNoteWidget() {
|
|
|
958
1128
|
headers: { "content-type": "application/json" },
|
|
959
1129
|
body: JSON.stringify({
|
|
960
1130
|
title: titleInput.value.trim(),
|
|
961
|
-
|
|
1131
|
+
tags: tagEditor.getTags(),
|
|
962
1132
|
text
|
|
963
1133
|
}),
|
|
964
1134
|
signal: AbortSignal.timeout(1e4)
|
|
@@ -992,7 +1162,7 @@ function mountNoteWidget() {
|
|
|
992
1162
|
const doEdit = async () => {
|
|
993
1163
|
const title = titleInput.value.trim().length > 0 ? titleInput.value.trim() : timestampTitle();
|
|
994
1164
|
const text = textarea.value;
|
|
995
|
-
const
|
|
1165
|
+
const tags = tagEditor.getTags();
|
|
996
1166
|
edit.disabled = true;
|
|
997
1167
|
edit.textContent = "打开中…";
|
|
998
1168
|
try {
|
|
@@ -1001,7 +1171,7 @@ function mountNoteWidget() {
|
|
|
1001
1171
|
headers: { "content-type": "application/json" },
|
|
1002
1172
|
body: JSON.stringify({
|
|
1003
1173
|
title,
|
|
1004
|
-
|
|
1174
|
+
tags,
|
|
1005
1175
|
text
|
|
1006
1176
|
}),
|
|
1007
1177
|
signal: AbortSignal.timeout(1e4)
|
|
@@ -1027,10 +1197,27 @@ function mountNoteWidget() {
|
|
|
1027
1197
|
edit.addEventListener("click", () => {
|
|
1028
1198
|
doEdit();
|
|
1029
1199
|
});
|
|
1030
|
-
return
|
|
1031
|
-
|
|
1200
|
+
return root;
|
|
1201
|
+
}
|
|
1202
|
+
/**
|
|
1203
|
+
* Mount the floating quick-note widget. Fetches /status first: when
|
|
1204
|
+
* `ui.showQuickNote` is off the widget is never created (no DOM side effects).
|
|
1205
|
+
* Returns a disposer that removes it.
|
|
1206
|
+
*/
|
|
1207
|
+
function mountNoteWidget() {
|
|
1208
|
+
let disposed = false;
|
|
1209
|
+
let root;
|
|
1210
|
+
const disposer = () => {
|
|
1211
|
+
disposed = true;
|
|
1212
|
+
root?.remove();
|
|
1032
1213
|
document.querySelector(".dsh-tw-toast")?.remove();
|
|
1033
1214
|
};
|
|
1215
|
+
(async () => {
|
|
1216
|
+
const ui = await fetchUiOptions();
|
|
1217
|
+
if (disposed || !ui.showQuickNote) return;
|
|
1218
|
+
root = buildWidget();
|
|
1219
|
+
})();
|
|
1220
|
+
return disposer;
|
|
1034
1221
|
}
|
|
1035
1222
|
//#endregion
|
|
1036
1223
|
//#region src/client/settings-page.ts
|
|
@@ -1148,6 +1335,7 @@ function renderConfigSection(body, config, refresh) {
|
|
|
1148
1335
|
section.append(make("h3", "dsh-tw-settings-h", "常规配置"));
|
|
1149
1336
|
const note = config.note ?? {};
|
|
1150
1337
|
const git = config.git ?? {};
|
|
1338
|
+
const ui = config.ui ?? {};
|
|
1151
1339
|
const fields = [];
|
|
1152
1340
|
const textField = (key, label, initial) => {
|
|
1153
1341
|
const input = make("input", "dsh-tw-settings-input");
|
|
@@ -1198,6 +1386,8 @@ function renderConfigSection(body, config, refresh) {
|
|
|
1198
1386
|
numField("git.debounceMs", "自动 commit 防抖(ms)", typeof git.debounceMs === "number" ? git.debounceMs : 6e4);
|
|
1199
1387
|
textField("git.remote", "git 远端(空=仅本地)", typeof git.remote === "string" ? git.remote : "");
|
|
1200
1388
|
textField("git.branch", "git 分支", typeof git.branch === "string" ? git.branch : "main");
|
|
1389
|
+
checkField("ui.showQuickNote", "显示「快速笔记」悬浮按钮", ui.showQuickNote !== false);
|
|
1390
|
+
checkField("ui.sidebarLeftCss", "侧边栏移左 CSS(随启动补丁还原被删除的样式)", ui.sidebarLeftCss !== false);
|
|
1201
1391
|
const save = make("button", "dsh-tw-settings-btn dsh-tw-settings-primary", "保存配置");
|
|
1202
1392
|
save.type = "button";
|
|
1203
1393
|
save.addEventListener("click", () => {
|
package/lib/client.js
CHANGED
|
@@ -54,6 +54,21 @@ const CSS_TEXT = `
|
|
|
54
54
|
.dsh-tw-entry[data-active="true"] { background: var(--dsw-alias-interactive-bg-active, rgba(128,128,128,.18)); color: var(--dsw-alias-label-primary, inherit); font-weight: 500; }
|
|
55
55
|
.dsh-tw-entry svg { flex: none; }
|
|
56
56
|
.dsh-tw-entry .dsh-tw-entry-label { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
57
|
+
/* Collapsed rail: the shell narrows the sidebar to a 56px icon rail (dual
|
|
58
|
+
signals — the frame's data-sidebar-collapsed + the sidebar root's hashed
|
|
59
|
+
*_collapsed class, same doctrine as dsh-taskboard 0.4.3). Mirror the
|
|
60
|
+
native 36×36 icon-button geometry: center the icon, hide the label, and
|
|
61
|
+
scale it up to match the shell's 18px rail icons. */
|
|
62
|
+
[data-sidebar-collapsed] [data-dsh-tw-entry],
|
|
63
|
+
[class*="_collapsed"] [data-dsh-tw-entry] {
|
|
64
|
+
width: 36px; height: 36px; min-width: 36px;
|
|
65
|
+
margin: 0 0 12px; padding: 0;
|
|
66
|
+
justify-content: center; gap: 0; text-align: center;
|
|
67
|
+
}
|
|
68
|
+
[data-sidebar-collapsed] [data-dsh-tw-entry] .dsh-tw-entry-label,
|
|
69
|
+
[class*="_collapsed"] [data-dsh-tw-entry] .dsh-tw-entry-label { display: none; }
|
|
70
|
+
[data-sidebar-collapsed] [data-dsh-tw-entry] svg,
|
|
71
|
+
[class*="_collapsed"] [data-dsh-tw-entry] svg { width: 18px; height: 18px; }
|
|
57
72
|
|
|
58
73
|
/* ── center-column panel (fixed overlay, JS-pinned to the column rect) ── */
|
|
59
74
|
.dsh-tw-view {
|
|
@@ -169,6 +184,34 @@ html[data-dsh-tw-active] .dshDesktopConversationSurface > :not([data-dsh-tw-view
|
|
|
169
184
|
}
|
|
170
185
|
.dsh-tw-note-edit:hover:not(:disabled) { filter: brightness(.96); }
|
|
171
186
|
.dsh-tw-note-edit:disabled { opacity: .55; cursor: default; }
|
|
187
|
+
/* ── Multi-tag chip editor (quick-note tags) ─────────────────────────────── */
|
|
188
|
+
.dsh-tw-note-tags { position: relative; flex: 1; min-width: 0; display: flex; flex-direction: column; gap: 4px; }
|
|
189
|
+
.dsh-tw-note-chips { display: flex; flex-wrap: wrap; gap: 4px; }
|
|
190
|
+
.dsh-tw-note-tagchip {
|
|
191
|
+
display: inline-flex; align-items: center; gap: 4px;
|
|
192
|
+
padding: 2px 6px 2px 8px; border-radius: 999px; font-size: 11px; line-height: 1.4;
|
|
193
|
+
background: color-mix(in srgb, var(--dsw-alias-brand-primary, #3e63dd) 10%, var(--dsw-alias-bg-layer-1, #fff));
|
|
194
|
+
border: 1px solid color-mix(in srgb, var(--dsw-alias-brand-primary, #3e63dd) 30%, transparent);
|
|
195
|
+
color: var(--dsw-alias-label-primary, #222);
|
|
196
|
+
}
|
|
197
|
+
.dsh-tw-note-tagchip-x {
|
|
198
|
+
cursor: pointer; font-size: 12px; line-height: 1; padding: 0 2px; border-radius: 50%;
|
|
199
|
+
color: var(--dsw-alias-label-secondary, #888);
|
|
200
|
+
}
|
|
201
|
+
.dsh-tw-note-tagchip-x:hover { color: var(--dsw-alias-state-error-primary, #d13b3b); }
|
|
202
|
+
.dsh-tw-note-taginput { width: 100%; box-sizing: border-box; }
|
|
203
|
+
.dsh-tw-note-tagsuggest {
|
|
204
|
+
position: absolute; left: 0; right: 0; top: calc(100% + 2px); z-index: 60;
|
|
205
|
+
max-height: 168px; overflow-y: auto; border-radius: 8px; padding: 4px;
|
|
206
|
+
background: var(--dsw-alias-bg-layer-2, #fff); color: var(--dsw-alias-label-primary, #222);
|
|
207
|
+
border: 1px solid var(--dsw-alias-border-l2, rgba(0,0,0,.18));
|
|
208
|
+
box-shadow: var(--dsw-shadow-lv2, 0 4px 16px rgba(0,0,0,.16));
|
|
209
|
+
font-size: 12px;
|
|
210
|
+
}
|
|
211
|
+
.dsh-tw-note-tagsuggest-item {
|
|
212
|
+
padding: 6px 9px; border-radius: 6px; cursor: pointer; white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
|
|
213
|
+
}
|
|
214
|
+
.dsh-tw-note-tagsuggest-item:hover { background: var(--dsw-alias-interactive-bg-hover, rgba(128,128,128,.12)); }
|
|
172
215
|
/* ── Native-editor popup iframe (quick-note "在 TW 中编辑") ──────────────── */
|
|
173
216
|
.dsh-tw-editor-popup {
|
|
174
217
|
position: fixed; left: 0; right: 0; top: 0; bottom: 0; margin: auto;
|
|
@@ -853,14 +896,23 @@ function ensurePopup() {
|
|
|
853
896
|
* collapsible, independent of any shell DOM. Lets the human jot drafts /
|
|
854
897
|
* scratch notes while waiting for the AI or drafting the next prompt.
|
|
855
898
|
*
|
|
856
|
-
* Save posts to /dsh-tiddlywiki/note → an independent tiddler (title &
|
|
899
|
+
* Save posts to /dsh-tiddlywiki/note → an independent tiddler (title & tags
|
|
857
900
|
* editable; defaults: timestamp title + config tag, usually "inbox").
|
|
858
901
|
*
|
|
902
|
+
* ui.showQuickNote config: the widget is mounted async and stays hidden (and
|
|
903
|
+
* never appended) when the option is off, so the toggle button can be disabled
|
|
904
|
+
* from the settings page without touching the shell.
|
|
905
|
+
*
|
|
906
|
+
* Tag editor: multi-select chips + autocomplete from the wiki's existing tags
|
|
907
|
+
* (GET /dsh-tiddlywiki/tags). Enter/`,` commits the typed value; Backspace on
|
|
908
|
+
* an empty field removes the last chip; the dropdown filters on typing.
|
|
909
|
+
*
|
|
859
910
|
* @module dsh-tiddlywiki/client/note-widget
|
|
860
911
|
*/
|
|
861
912
|
const NOTE_ENDPOINT = "/dsh-tiddlywiki/note";
|
|
862
913
|
const EDIT_ENDPOINT = "/dsh-tiddlywiki/edit";
|
|
863
914
|
const STATUS_ENDPOINT = "/dsh-tiddlywiki/status";
|
|
915
|
+
const TAGS_ENDPOINT = "/dsh-tiddlywiki/tags";
|
|
864
916
|
function pad(n) {
|
|
865
917
|
return n < 10 ? `0${n}` : String(n);
|
|
866
918
|
}
|
|
@@ -868,16 +920,137 @@ function pad(n) {
|
|
|
868
920
|
function timestampTitle(date = /* @__PURE__ */ new Date()) {
|
|
869
921
|
return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())} ${pad(date.getHours())}:${pad(date.getMinutes())}`;
|
|
870
922
|
}
|
|
871
|
-
async function
|
|
923
|
+
async function fetchUiOptions() {
|
|
924
|
+
const fallback = {
|
|
925
|
+
showQuickNote: true,
|
|
926
|
+
defaultTag: "inbox"
|
|
927
|
+
};
|
|
872
928
|
try {
|
|
873
929
|
const res = await fetch(STATUS_ENDPOINT, { signal: AbortSignal.timeout(5e3) });
|
|
874
|
-
if (!res.ok) return
|
|
875
|
-
|
|
930
|
+
if (!res.ok) return fallback;
|
|
931
|
+
const payload = await res.json();
|
|
932
|
+
return {
|
|
933
|
+
showQuickNote: payload.ui?.showQuickNote !== false,
|
|
934
|
+
defaultTag: payload.note?.tag ?? "inbox"
|
|
935
|
+
};
|
|
876
936
|
} catch {
|
|
877
|
-
return
|
|
937
|
+
return fallback;
|
|
878
938
|
}
|
|
879
939
|
}
|
|
880
|
-
|
|
940
|
+
/** Multi-tag chip editor with autocomplete from the wiki's existing tags. */
|
|
941
|
+
function buildTagEditor() {
|
|
942
|
+
const wrap = document.createElement("div");
|
|
943
|
+
wrap.className = "dsh-tw-note-tags";
|
|
944
|
+
const chipWrap = document.createElement("div");
|
|
945
|
+
chipWrap.className = "dsh-tw-note-chips";
|
|
946
|
+
const input = document.createElement("input");
|
|
947
|
+
input.className = "dsh-tw-note-taginput";
|
|
948
|
+
input.placeholder = "tag(可多选,自动补全)";
|
|
949
|
+
const suggest = document.createElement("div");
|
|
950
|
+
suggest.className = "dsh-tw-note-tagsuggest";
|
|
951
|
+
suggest.hidden = true;
|
|
952
|
+
wrap.append(chipWrap, input, suggest);
|
|
953
|
+
const chips = [];
|
|
954
|
+
const hideSuggest = () => {
|
|
955
|
+
suggest.hidden = true;
|
|
956
|
+
};
|
|
957
|
+
const renderChips = () => {
|
|
958
|
+
chipWrap.replaceChildren();
|
|
959
|
+
for (const tag of chips) {
|
|
960
|
+
const chip = document.createElement("span");
|
|
961
|
+
chip.className = "dsh-tw-note-tagchip";
|
|
962
|
+
chip.textContent = tag;
|
|
963
|
+
const x = document.createElement("span");
|
|
964
|
+
x.className = "dsh-tw-note-tagchip-x";
|
|
965
|
+
x.textContent = "×";
|
|
966
|
+
x.title = `移除 tag「${tag}」`;
|
|
967
|
+
x.addEventListener("click", (event) => {
|
|
968
|
+
event.stopPropagation();
|
|
969
|
+
const i = chips.indexOf(tag);
|
|
970
|
+
if (i >= 0) {
|
|
971
|
+
chips.splice(i, 1);
|
|
972
|
+
renderChips();
|
|
973
|
+
}
|
|
974
|
+
});
|
|
975
|
+
chip.append(x);
|
|
976
|
+
chipWrap.append(chip);
|
|
977
|
+
}
|
|
978
|
+
};
|
|
979
|
+
const addTag = (tag) => {
|
|
980
|
+
const t = tag.trim();
|
|
981
|
+
if (t.length === 0 || chips.includes(t)) return;
|
|
982
|
+
chips.push(t);
|
|
983
|
+
input.value = "";
|
|
984
|
+
renderChips();
|
|
985
|
+
hideSuggest();
|
|
986
|
+
input.focus();
|
|
987
|
+
};
|
|
988
|
+
const commitInput = () => {
|
|
989
|
+
for (const raw of input.value.split(/\s+/)) addTag(raw);
|
|
990
|
+
};
|
|
991
|
+
let knownTags = [];
|
|
992
|
+
let tagsPromise;
|
|
993
|
+
const ensureTags = () => {
|
|
994
|
+
tagsPromise ??= fetch(TAGS_ENDPOINT, { signal: AbortSignal.timeout(5e3) }).then((r) => r.ok ? r.json() : Promise.resolve({})).then((p) => [...p.tags ?? []].sort((a, b) => a.localeCompare(b, "zh"))).catch(() => []);
|
|
995
|
+
tagsPromise.then((list) => {
|
|
996
|
+
knownTags = list;
|
|
997
|
+
});
|
|
998
|
+
return tagsPromise;
|
|
999
|
+
};
|
|
1000
|
+
const showSuggest = () => {
|
|
1001
|
+
const q = input.value.trim().toLowerCase();
|
|
1002
|
+
const matches = knownTags.filter((t) => !chips.includes(t) && (q.length === 0 || t.toLowerCase().includes(q))).slice(0, 8);
|
|
1003
|
+
suggest.replaceChildren();
|
|
1004
|
+
for (const tag of matches) {
|
|
1005
|
+
const item = document.createElement("div");
|
|
1006
|
+
item.className = "dsh-tw-note-tagsuggest-item";
|
|
1007
|
+
item.textContent = tag;
|
|
1008
|
+
item.addEventListener("mousedown", (event) => {
|
|
1009
|
+
event.preventDefault();
|
|
1010
|
+
addTag(tag);
|
|
1011
|
+
});
|
|
1012
|
+
suggest.append(item);
|
|
1013
|
+
}
|
|
1014
|
+
suggest.hidden = matches.length === 0;
|
|
1015
|
+
};
|
|
1016
|
+
input.addEventListener("focus", () => {
|
|
1017
|
+
ensureTags().then(showSuggest);
|
|
1018
|
+
});
|
|
1019
|
+
input.addEventListener("input", () => {
|
|
1020
|
+
if (knownTags.length === 0) ensureTags().then(showSuggest);
|
|
1021
|
+
else showSuggest();
|
|
1022
|
+
});
|
|
1023
|
+
input.addEventListener("keydown", (event) => {
|
|
1024
|
+
if (event.key === "Enter" || event.key === ",") {
|
|
1025
|
+
event.preventDefault();
|
|
1026
|
+
commitInput();
|
|
1027
|
+
} else if (event.key === "Backspace" && input.value.length === 0 && chips.length > 0) {
|
|
1028
|
+
chips.pop();
|
|
1029
|
+
renderChips();
|
|
1030
|
+
} else if (event.key === "Escape") hideSuggest();
|
|
1031
|
+
});
|
|
1032
|
+
document.addEventListener("click", (event) => {
|
|
1033
|
+
if (!wrap.contains(event.target)) hideSuggest();
|
|
1034
|
+
}, true);
|
|
1035
|
+
return {
|
|
1036
|
+
el: wrap,
|
|
1037
|
+
getTags: () => {
|
|
1038
|
+
commitInput();
|
|
1039
|
+
return [...chips];
|
|
1040
|
+
},
|
|
1041
|
+
setDefault: (tag) => {
|
|
1042
|
+
if (chips.length === 0) {
|
|
1043
|
+
const t = tag.trim();
|
|
1044
|
+
if (t.length > 0) {
|
|
1045
|
+
chips.push(t);
|
|
1046
|
+
renderChips();
|
|
1047
|
+
}
|
|
1048
|
+
}
|
|
1049
|
+
}
|
|
1050
|
+
};
|
|
1051
|
+
}
|
|
1052
|
+
/** Build the whole widget DOM and wire it up (returns the appended root). */
|
|
1053
|
+
function buildWidget() {
|
|
881
1054
|
const root = document.createElement("div");
|
|
882
1055
|
root.className = "dsh-tw-note";
|
|
883
1056
|
const card = document.createElement("div");
|
|
@@ -899,10 +1072,8 @@ function mountNoteWidget() {
|
|
|
899
1072
|
const titleInput = document.createElement("input");
|
|
900
1073
|
titleInput.className = "dsh-tw-note-title";
|
|
901
1074
|
titleInput.placeholder = "标题(默认时间戳)";
|
|
902
|
-
const
|
|
903
|
-
|
|
904
|
-
tagInput.placeholder = "tag(默认 inbox)";
|
|
905
|
-
fields.append(titleInput, tagInput);
|
|
1075
|
+
const tagEditor = buildTagEditor();
|
|
1076
|
+
fields.append(titleInput, tagEditor.el);
|
|
906
1077
|
const textarea = document.createElement("textarea");
|
|
907
1078
|
textarea.className = "dsh-tw-note-text";
|
|
908
1079
|
textarea.placeholder = "写点东西…(Ctrl+Enter 保存)";
|
|
@@ -937,10 +1108,9 @@ function mountNoteWidget() {
|
|
|
937
1108
|
card.hidden = false;
|
|
938
1109
|
opened = true;
|
|
939
1110
|
resetTitle();
|
|
940
|
-
if (tagInput.value.length === 0) tagInput.value = defaultTag;
|
|
941
1111
|
textarea.focus();
|
|
942
|
-
defaultTag = await
|
|
943
|
-
|
|
1112
|
+
defaultTag = (await fetchUiOptions()).defaultTag;
|
|
1113
|
+
tagEditor.setDefault(defaultTag);
|
|
944
1114
|
};
|
|
945
1115
|
const close = () => {
|
|
946
1116
|
card.hidden = true;
|
|
@@ -964,7 +1134,7 @@ function mountNoteWidget() {
|
|
|
964
1134
|
headers: { "content-type": "application/json" },
|
|
965
1135
|
body: JSON.stringify({
|
|
966
1136
|
title: titleInput.value.trim(),
|
|
967
|
-
|
|
1137
|
+
tags: tagEditor.getTags(),
|
|
968
1138
|
text
|
|
969
1139
|
}),
|
|
970
1140
|
signal: AbortSignal.timeout(1e4)
|
|
@@ -998,7 +1168,7 @@ function mountNoteWidget() {
|
|
|
998
1168
|
const doEdit = async () => {
|
|
999
1169
|
const title = titleInput.value.trim().length > 0 ? titleInput.value.trim() : timestampTitle();
|
|
1000
1170
|
const text = textarea.value;
|
|
1001
|
-
const
|
|
1171
|
+
const tags = tagEditor.getTags();
|
|
1002
1172
|
edit.disabled = true;
|
|
1003
1173
|
edit.textContent = "打开中…";
|
|
1004
1174
|
try {
|
|
@@ -1007,7 +1177,7 @@ function mountNoteWidget() {
|
|
|
1007
1177
|
headers: { "content-type": "application/json" },
|
|
1008
1178
|
body: JSON.stringify({
|
|
1009
1179
|
title,
|
|
1010
|
-
|
|
1180
|
+
tags,
|
|
1011
1181
|
text
|
|
1012
1182
|
}),
|
|
1013
1183
|
signal: AbortSignal.timeout(1e4)
|
|
@@ -1033,10 +1203,27 @@ function mountNoteWidget() {
|
|
|
1033
1203
|
edit.addEventListener("click", () => {
|
|
1034
1204
|
doEdit();
|
|
1035
1205
|
});
|
|
1036
|
-
return
|
|
1037
|
-
|
|
1206
|
+
return root;
|
|
1207
|
+
}
|
|
1208
|
+
/**
|
|
1209
|
+
* Mount the floating quick-note widget. Fetches /status first: when
|
|
1210
|
+
* `ui.showQuickNote` is off the widget is never created (no DOM side effects).
|
|
1211
|
+
* Returns a disposer that removes it.
|
|
1212
|
+
*/
|
|
1213
|
+
function mountNoteWidget() {
|
|
1214
|
+
let disposed = false;
|
|
1215
|
+
let root;
|
|
1216
|
+
const disposer = () => {
|
|
1217
|
+
disposed = true;
|
|
1218
|
+
root?.remove();
|
|
1038
1219
|
document.querySelector(".dsh-tw-toast")?.remove();
|
|
1039
1220
|
};
|
|
1221
|
+
(async () => {
|
|
1222
|
+
const ui = await fetchUiOptions();
|
|
1223
|
+
if (disposed || !ui.showQuickNote) return;
|
|
1224
|
+
root = buildWidget();
|
|
1225
|
+
})();
|
|
1226
|
+
return disposer;
|
|
1040
1227
|
}
|
|
1041
1228
|
//#endregion
|
|
1042
1229
|
//#region src/client/settings-page.ts
|
|
@@ -1154,6 +1341,7 @@ function renderConfigSection(body, config, refresh) {
|
|
|
1154
1341
|
section.append(make("h3", "dsh-tw-settings-h", "常规配置"));
|
|
1155
1342
|
const note = config.note ?? {};
|
|
1156
1343
|
const git = config.git ?? {};
|
|
1344
|
+
const ui = config.ui ?? {};
|
|
1157
1345
|
const fields = [];
|
|
1158
1346
|
const textField = (key, label, initial) => {
|
|
1159
1347
|
const input = make("input", "dsh-tw-settings-input");
|
|
@@ -1204,6 +1392,8 @@ function renderConfigSection(body, config, refresh) {
|
|
|
1204
1392
|
numField("git.debounceMs", "自动 commit 防抖(ms)", typeof git.debounceMs === "number" ? git.debounceMs : 6e4);
|
|
1205
1393
|
textField("git.remote", "git 远端(空=仅本地)", typeof git.remote === "string" ? git.remote : "");
|
|
1206
1394
|
textField("git.branch", "git 分支", typeof git.branch === "string" ? git.branch : "main");
|
|
1395
|
+
checkField("ui.showQuickNote", "显示「快速笔记」悬浮按钮", ui.showQuickNote !== false);
|
|
1396
|
+
checkField("ui.sidebarLeftCss", "侧边栏移左 CSS(随启动补丁还原被删除的样式)", ui.sidebarLeftCss !== false);
|
|
1207
1397
|
const save = make("button", "dsh-tw-settings-btn dsh-tw-settings-primary", "保存配置");
|
|
1208
1398
|
save.type = "button";
|
|
1209
1399
|
save.addEventListener("click", () => {
|