dsh-tiddlywiki 0.7.3 → 0.8.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 +25 -1
- package/lib/client.bundle.js +264 -2
- package/lib/client.js +264 -2
- package/lib/index.js +83 -26
- package/lib/index.js.map +1 -1
- package/package.json +1 -1
- package/src/client/editor-popup.ts +127 -121
- package/src/client/panel.ts +12 -1
- package/src/client/settings-page.ts +2 -0
- package/src/client/styles.ts +4 -2
- package/src/client/theme-sync.ts +312 -0
- package/src/host/config.ts +14 -0
- package/src/host/routes.ts +36 -6
- package/src/host/tools.ts +41 -12
- package/src/index.ts +22 -11
package/lib/client.js
CHANGED
|
@@ -107,7 +107,9 @@ html[data-dsh-tw-active] .dshDesktopConversationSurface > :not([data-dsh-tw-view
|
|
|
107
107
|
.dsh-tw-status-dot[data-state="starting"] { background: var(--dsw-alias-state-warning-primary, #d9822b); }
|
|
108
108
|
.dsh-tw-status-dot[data-state="failed"], .dsh-tw-status-dot[data-state="stopped"] { background: var(--dsw-alias-state-error-primary, #d13b3b); }
|
|
109
109
|
|
|
110
|
-
|
|
110
|
+
/* The iframe background follows the DSH theme so a blank/preload frame never
|
|
111
|
+
flashes pure white in dark mode (TW paints its own palette once loaded). */
|
|
112
|
+
.dsh-tw-panel-frame { flex: 1; min-height: 0; border: 0; width: 100%; display: block; background: var(--dsw-alias-bg-layer-1, #fff); }
|
|
111
113
|
/* [hidden] must beat the author display rules above (UA hidden is overridden). */
|
|
112
114
|
.dsh-tw-panel-frame[hidden],
|
|
113
115
|
.dsh-tw-panel-error[hidden] { display: none !important; }
|
|
@@ -287,7 +289,7 @@ html[data-dsh-tw-active] .dshDesktopConversationSurface > :not([data-dsh-tw-view
|
|
|
287
289
|
color: var(--dsw-alias-label-dimmed, #888); padding: 5px 8px; border-radius: 6px;
|
|
288
290
|
}
|
|
289
291
|
.dsh-tw-editor-close:hover { background: var(--dsw-alias-interactive-bg-hover, rgba(128,128,128,.12)); color: var(--dsw-alias-label-primary, #222); }
|
|
290
|
-
.dsh-tw-editor-frame { flex: 1; min-height: 0; width: 100%; border: 0; background: #fff; }
|
|
292
|
+
.dsh-tw-editor-frame { flex: 1; min-height: 0; width: 100%; border: 0; background: var(--dsw-alias-bg-layer-1, #fff); }
|
|
291
293
|
.dsh-tw-editor-resize { position: absolute; right: 0; bottom: 0; width: 16px; height: 16px; cursor: nwse-resize; }
|
|
292
294
|
.dsh-tw-note-toggle {
|
|
293
295
|
border: 1px solid var(--dsw-alias-border-l2, rgba(0,0,0,.18));
|
|
@@ -643,6 +645,253 @@ function mountSidebarEntry(state) {
|
|
|
643
645
|
};
|
|
644
646
|
}
|
|
645
647
|
//#endregion
|
|
648
|
+
//#region src/client/theme-sync.ts
|
|
649
|
+
/**
|
|
650
|
+
* Adaptive DSH theme for the embedded TiddlyWiki iframes (design doc §12).
|
|
651
|
+
*
|
|
652
|
+
* Both the center-column panel and the quick-note editor popup embed the
|
|
653
|
+
* SAME-ORIGIN TW proxy, so this module can reach the TW runtime directly
|
|
654
|
+
* (`iframe.contentWindow.$tw`) and drive its native theming.
|
|
655
|
+
*
|
|
656
|
+
* How TW theming works
|
|
657
|
+
* --------------------
|
|
658
|
+
* TW 5 renders its colours through the ACTIVE palette: `$:/palette` holds the
|
|
659
|
+
* title of a palette tiddler (`$:/palettes/…`), and every `<<colour x>>` in
|
|
660
|
+
* the theme stylesheet resolves against that tiddler at render time. TW
|
|
661
|
+
* re-renders all stylesheets live on any tiddler change, so flipping
|
|
662
|
+
* `$:/palette` re-themes the whole embedded UI instantly — the exact mechanism
|
|
663
|
+
* TW's own palette switcher uses. Overriding the rendered CSS variables is a
|
|
664
|
+
* dead end for the default Vanilla theme because its base stylesheet bakes the
|
|
665
|
+
* palette values in as literals; switching the palette is the supported path.
|
|
666
|
+
*
|
|
667
|
+
* DSH theme detection
|
|
668
|
+
* -------------------
|
|
669
|
+
* The DSH shell's ThemePresenter projects the resolved theme onto the page:
|
|
670
|
+
* `data-ds-dark-theme` on <body> (dark) plus an inline `color-scheme` on
|
|
671
|
+
* <html> that is always "light"|"dark" (the "system" preference is resolved
|
|
672
|
+
* upstream). Those two signals are authoritative; `prefers-color-scheme` is
|
|
673
|
+
* only a fallback when neither is present (no DSH theming projected).
|
|
674
|
+
*
|
|
675
|
+
* Non-persistence
|
|
676
|
+
* ---------------
|
|
677
|
+
* `$:/palette` IS included in TW's default sync filter, so a naive write would
|
|
678
|
+
* be PUT back to the TiddlyWeb server and land in the wiki's git history on
|
|
679
|
+
* every theme toggle. We suppress that: the syncer's dirty check is
|
|
680
|
+
* `getChangeCount(title) > tiddlerInfo[title].changeCount`, and the change
|
|
681
|
+
* event that would drive the save is dispatched on a microtask — so after
|
|
682
|
+
* writing we synchronously re-align the recorded changeCount and the syncer
|
|
683
|
+
* sees the tiddler as already clean. The flip stays in-memory only; the wiki's
|
|
684
|
+
* stored palette and the user's choice are never modified.
|
|
685
|
+
*
|
|
686
|
+
* @module dsh-tiddlywiki/client/theme-sync
|
|
687
|
+
*/
|
|
688
|
+
/** Default dark palette used when DSH is dark (overridable in settings). */
|
|
689
|
+
const DARK_PALETTE_DEFAULT = "$:/palettes/CupertinoDark";
|
|
690
|
+
/** Fallback light palette when no user palette was ever captured. */
|
|
691
|
+
const LIGHT_PALETTE_FALLBACK = "$:/palettes/Vanilla";
|
|
692
|
+
/** DSH's dark-mode marker (set/removed on <body> by dsh-client-ui-layout). */
|
|
693
|
+
const DARK_ATTR = "data-ds-dark-theme";
|
|
694
|
+
/** The TW tiddler holding the active palette tiddler title. */
|
|
695
|
+
const PALETTE_TIDDLER = "$:/palette";
|
|
696
|
+
/** Appliers registered for live frames; config changes re-apply to all. */
|
|
697
|
+
const frameAppliers = /* @__PURE__ */ new Set();
|
|
698
|
+
/** Syncers already wrapped with the palette save guard (per iframe window). */
|
|
699
|
+
const guardedSyncers = /* @__PURE__ */ new WeakSet();
|
|
700
|
+
/** Remembered user palette (light) that we restore when leaving dark mode. */
|
|
701
|
+
let userLightPalette = "";
|
|
702
|
+
/** Whether the adaptive feature is on (settings `ui.followDshTheme`). */
|
|
703
|
+
let syncEnabled = true;
|
|
704
|
+
/** Configured dark palette (settings `ui.darkPalette`). */
|
|
705
|
+
let darkPalette = DARK_PALETTE_DEFAULT;
|
|
706
|
+
/**
|
|
707
|
+
* Resolve the DSH shell's current effective dark state. The body attribute
|
|
708
|
+
* and the inline root `color-scheme` projected by the ThemePresenter are
|
|
709
|
+
* authoritative; the OS media query only answers when DSH isn't projecting a
|
|
710
|
+
* theme snapshot (plain embed / non-DSH page).
|
|
711
|
+
*/
|
|
712
|
+
function readDshDark() {
|
|
713
|
+
try {
|
|
714
|
+
const root = document.documentElement;
|
|
715
|
+
const body = document.body;
|
|
716
|
+
if (body !== null && body.hasAttribute(DARK_ATTR)) return true;
|
|
717
|
+
if (root !== null) {
|
|
718
|
+
const scheme = root.style.getPropertyValue("color-scheme").trim().toLowerCase();
|
|
719
|
+
if (scheme === "dark") return true;
|
|
720
|
+
if (scheme === "light") return false;
|
|
721
|
+
}
|
|
722
|
+
} catch {}
|
|
723
|
+
try {
|
|
724
|
+
return window.matchMedia("(prefers-color-scheme: dark)").matches;
|
|
725
|
+
} catch {
|
|
726
|
+
return false;
|
|
727
|
+
}
|
|
728
|
+
}
|
|
729
|
+
/** Active palette tiddler title (falls back to the light default). */
|
|
730
|
+
function activePaletteTitle($tw) {
|
|
731
|
+
try {
|
|
732
|
+
const title = $tw.wiki.getTiddlerText(PALETTE_TIDDLER);
|
|
733
|
+
return typeof title === "string" && title.trim().length > 0 ? title.trim() : LIGHT_PALETTE_FALLBACK;
|
|
734
|
+
} catch {
|
|
735
|
+
return LIGHT_PALETTE_FALLBACK;
|
|
736
|
+
}
|
|
737
|
+
}
|
|
738
|
+
/** True when the active palette tiddler declares `color-scheme: dark`. */
|
|
739
|
+
function activePaletteIsDark($tw) {
|
|
740
|
+
try {
|
|
741
|
+
const fields = $tw.wiki.getTiddler(activePaletteTitle($tw))?.fields;
|
|
742
|
+
return fields !== void 0 && fields["color-scheme"] === "dark";
|
|
743
|
+
} catch {
|
|
744
|
+
return false;
|
|
745
|
+
}
|
|
746
|
+
}
|
|
747
|
+
/**
|
|
748
|
+
* Mark `$:/palette` as clean in the syncer's records so a FORCED flip is never
|
|
749
|
+
* PUT back to the TiddlyWeb server. Runs synchronously right after the write,
|
|
750
|
+
* before the microtask-dispatched change event lets the syncer evaluate
|
|
751
|
+
* dirtiness; user-initiated palette changes (bumped afterwards) still sync.
|
|
752
|
+
*/
|
|
753
|
+
function suppressPaletteSync($tw) {
|
|
754
|
+
try {
|
|
755
|
+
const syncer = $tw.syncer;
|
|
756
|
+
if (syncer?.tiddlerInfo === void 0) return;
|
|
757
|
+
const changeCount = $tw.wiki.getChangeCount(PALETTE_TIDDLER);
|
|
758
|
+
const info = syncer.tiddlerInfo[PALETTE_TIDDLER];
|
|
759
|
+
if (info !== void 0) info.changeCount = changeCount;
|
|
760
|
+
else syncer.tiddlerInfo[PALETTE_TIDDLER] = { changeCount };
|
|
761
|
+
} catch {}
|
|
762
|
+
}
|
|
763
|
+
/**
|
|
764
|
+
* Install a one-shot guard on this frame's syncadaptor: while a palette WE
|
|
765
|
+
* forced is active (`userLightPalette` captured), a PUT of `$:/palette` whose
|
|
766
|
+
* text is still our forced dark palette is swallowed instead of reaching the
|
|
767
|
+
* TiddlyWeb server. This makes the non-persistence guarantee deterministic —
|
|
768
|
+
* TW's boot-time sync (which may load `$:/palette` from the server and bump its
|
|
769
|
+
* changeCount AFTER our re-align) can otherwise queue a save of the forced
|
|
770
|
+
* palette during the first ~1s. Idempotent per syncer instance.
|
|
771
|
+
*/
|
|
772
|
+
function installPaletteSaveGuard($tw) {
|
|
773
|
+
try {
|
|
774
|
+
const syncer = $tw.syncer;
|
|
775
|
+
const adaptor = syncer?.syncadaptor;
|
|
776
|
+
if (syncer === void 0 || adaptor === void 0 || guardedSyncers.has(syncer)) return;
|
|
777
|
+
const origSave = adaptor.saveTiddler.bind(adaptor);
|
|
778
|
+
adaptor.saveTiddler = (tiddler, callback) => {
|
|
779
|
+
if (syncEnabled && userLightPalette.length > 0 && tiddler?.fields?.title === PALETTE_TIDDLER && tiddler?.fields?.text === darkPalette) {
|
|
780
|
+
callback(null, {}, 0);
|
|
781
|
+
return;
|
|
782
|
+
}
|
|
783
|
+
origSave(tiddler, callback);
|
|
784
|
+
};
|
|
785
|
+
guardedSyncers.add(syncer);
|
|
786
|
+
} catch {}
|
|
787
|
+
}
|
|
788
|
+
/** Write the active palette (no-op when unchanged) without persisting. */
|
|
789
|
+
function setPalette($tw, target) {
|
|
790
|
+
try {
|
|
791
|
+
if (activePaletteTitle($tw) === target) return;
|
|
792
|
+
$tw.wiki.setText(PALETTE_TIDDLER, "text", void 0, target);
|
|
793
|
+
suppressPaletteSync($tw);
|
|
794
|
+
} catch {}
|
|
795
|
+
}
|
|
796
|
+
/**
|
|
797
|
+
* Drive `$:/palette` toward the DSH theme. In dark mode a light palette is
|
|
798
|
+
* swapped for `darkPalette` (remembering the user's palette first); in light
|
|
799
|
+
* mode a palette WE forced is restored, while a user-chosen dark palette is
|
|
800
|
+
* left alone.
|
|
801
|
+
*/
|
|
802
|
+
function applyPalette($tw, dark) {
|
|
803
|
+
const current = activePaletteTitle($tw);
|
|
804
|
+
if (dark) {
|
|
805
|
+
if (activePaletteIsDark($tw)) return;
|
|
806
|
+
if (current !== darkPalette) userLightPalette = current;
|
|
807
|
+
setPalette($tw, darkPalette);
|
|
808
|
+
} else if (activePaletteIsDark($tw) && current === darkPalette && userLightPalette.length > 0) setPalette($tw, userLightPalette);
|
|
809
|
+
}
|
|
810
|
+
/** Keep the iframe's native `color-scheme` in step with the ACTIVE palette. */
|
|
811
|
+
function syncColorScheme($tw, frame) {
|
|
812
|
+
try {
|
|
813
|
+
const html = frame.contentDocument?.documentElement;
|
|
814
|
+
if (html === void 0 || html === null) return;
|
|
815
|
+
const target = activePaletteIsDark($tw) ? "dark" : "light";
|
|
816
|
+
if (html.style.getPropertyValue("color-scheme") !== target) html.style.setProperty("color-scheme", target);
|
|
817
|
+
} catch {}
|
|
818
|
+
}
|
|
819
|
+
/** Apply the current DSH theme to one TW iframe (no-op until TW is ready). */
|
|
820
|
+
function applyToFrame(frame) {
|
|
821
|
+
const $tw = frame.contentWindow?.$tw;
|
|
822
|
+
if ($tw === void 0 || $tw.wiki === void 0) return;
|
|
823
|
+
installPaletteSaveGuard($tw);
|
|
824
|
+
if (syncEnabled) applyPalette($tw, readDshDark());
|
|
825
|
+
else applyPalette($tw, false);
|
|
826
|
+
syncColorScheme($tw, frame);
|
|
827
|
+
}
|
|
828
|
+
/**
|
|
829
|
+
* Configure the feature and re-apply to every live frame. `enabled` is the
|
|
830
|
+
* settings `ui.followDshTheme`; `darkPalette` the settings `ui.darkPalette`.
|
|
831
|
+
*/
|
|
832
|
+
function setThemeSyncConfig(opts) {
|
|
833
|
+
const enabled = opts.enabled !== false;
|
|
834
|
+
const palette = typeof opts.darkPalette === "string" && opts.darkPalette.trim().length > 0 ? opts.darkPalette.trim() : DARK_PALETTE_DEFAULT;
|
|
835
|
+
const changed = enabled !== syncEnabled || palette !== darkPalette;
|
|
836
|
+
syncEnabled = enabled;
|
|
837
|
+
darkPalette = palette;
|
|
838
|
+
if (changed) for (const apply of [...frameAppliers]) apply();
|
|
839
|
+
}
|
|
840
|
+
/**
|
|
841
|
+
* Make one TW iframe follow the DSH theme for as long as the returned
|
|
842
|
+
* disposer is not called. Re-applies on the iframe's `load` (covers TW
|
|
843
|
+
* restarts and proxy reloads), on DSH theme changes (body-attribute mutations
|
|
844
|
+
* and `prefers-color-scheme`), and once immediately for already-loaded frames.
|
|
845
|
+
*/
|
|
846
|
+
function attachThemeSync(frame) {
|
|
847
|
+
if (frame === null) return () => {};
|
|
848
|
+
const apply = () => applyToFrame(frame);
|
|
849
|
+
frameAppliers.add(apply);
|
|
850
|
+
const onLoad = () => apply();
|
|
851
|
+
frame.addEventListener("load", onLoad);
|
|
852
|
+
const media = window.matchMedia("(prefers-color-scheme: dark)");
|
|
853
|
+
const onMedia = () => apply();
|
|
854
|
+
try {
|
|
855
|
+
media.addEventListener("change", onMedia);
|
|
856
|
+
} catch {
|
|
857
|
+
media.addListener?.(onMedia);
|
|
858
|
+
}
|
|
859
|
+
let observer;
|
|
860
|
+
try {
|
|
861
|
+
observer = new MutationObserver(() => apply());
|
|
862
|
+
if (document.body !== null) observer.observe(document.body, {
|
|
863
|
+
attributes: true,
|
|
864
|
+
attributeFilter: [DARK_ATTR]
|
|
865
|
+
});
|
|
866
|
+
if (document.documentElement !== null) observer.observe(document.documentElement, {
|
|
867
|
+
attributes: true,
|
|
868
|
+
attributeFilter: ["style"]
|
|
869
|
+
});
|
|
870
|
+
} catch {
|
|
871
|
+
observer = void 0;
|
|
872
|
+
}
|
|
873
|
+
const timers = [
|
|
874
|
+
150,
|
|
875
|
+
600,
|
|
876
|
+
1500,
|
|
877
|
+
3e3
|
|
878
|
+
].map((ms) => window.setTimeout(apply, ms));
|
|
879
|
+
apply();
|
|
880
|
+
return () => {
|
|
881
|
+
frameAppliers.delete(apply);
|
|
882
|
+
frame.removeEventListener("load", onLoad);
|
|
883
|
+
for (const id of timers) window.clearTimeout(id);
|
|
884
|
+
try {
|
|
885
|
+
media.removeEventListener("change", onMedia);
|
|
886
|
+
} catch {
|
|
887
|
+
media.removeListener?.(onMedia);
|
|
888
|
+
}
|
|
889
|
+
try {
|
|
890
|
+
observer?.disconnect();
|
|
891
|
+
} catch {}
|
|
892
|
+
};
|
|
893
|
+
}
|
|
894
|
+
//#endregion
|
|
646
895
|
//#region src/client/panel.ts
|
|
647
896
|
/**
|
|
648
897
|
* Center-column targets, most-specific shell generation first. The official
|
|
@@ -730,6 +979,7 @@ function mountPanel(state) {
|
|
|
730
979
|
let errorArea;
|
|
731
980
|
let refreshTimer;
|
|
732
981
|
let refreshAttempts = 0;
|
|
982
|
+
let themeSyncDispose;
|
|
733
983
|
const build = () => {
|
|
734
984
|
const view = document.createElement("div");
|
|
735
985
|
view.dataset.dshTwView = "";
|
|
@@ -742,6 +992,7 @@ function mountPanel(state) {
|
|
|
742
992
|
iframe.title = "TiddlyWiki";
|
|
743
993
|
iframe.hidden = true;
|
|
744
994
|
frameArea.append(iframe);
|
|
995
|
+
themeSyncDispose = attachThemeSync(iframe);
|
|
745
996
|
errorArea = document.createElement("div");
|
|
746
997
|
errorArea.className = "dsh-tw-panel-error";
|
|
747
998
|
errorArea.hidden = true;
|
|
@@ -835,6 +1086,10 @@ function mountPanel(state) {
|
|
|
835
1086
|
}
|
|
836
1087
|
if (payload.status === "running") {
|
|
837
1088
|
refreshAttempts = 0;
|
|
1089
|
+
setThemeSyncConfig({
|
|
1090
|
+
enabled: payload.ui?.followDshTheme !== false,
|
|
1091
|
+
darkPalette: payload.ui?.darkPalette
|
|
1092
|
+
});
|
|
838
1093
|
if (typeof payload.twProxy === "string") showFrame(new URL(payload.twProxy, window.location.origin).href);
|
|
839
1094
|
else if (typeof payload.url === "string") showFrame(payload.url);
|
|
840
1095
|
else showError("服务未返回编辑器地址");
|
|
@@ -913,6 +1168,7 @@ function mountPanel(state) {
|
|
|
913
1168
|
document.removeEventListener("dsh-tw-panel-reload", onReloadRequest);
|
|
914
1169
|
waitObserver.disconnect();
|
|
915
1170
|
unsubscribe();
|
|
1171
|
+
themeSyncDispose?.();
|
|
916
1172
|
document.documentElement.removeAttribute(ACTIVE_ATTR);
|
|
917
1173
|
container?.remove();
|
|
918
1174
|
};
|
|
@@ -957,6 +1213,7 @@ function toast(message) {
|
|
|
957
1213
|
let root;
|
|
958
1214
|
let frame;
|
|
959
1215
|
let titleEl;
|
|
1216
|
+
let themeSyncDispose;
|
|
960
1217
|
/** Open (create on first use) the popup and load `url` (twUrl#draftTitle). */
|
|
961
1218
|
function openEditorPopup(url, label) {
|
|
962
1219
|
ensurePopup();
|
|
@@ -967,6 +1224,8 @@ function openEditorPopup(url, label) {
|
|
|
967
1224
|
}
|
|
968
1225
|
/** Remove the popup DOM entirely (plugin dispose). */
|
|
969
1226
|
function disposeEditorPopup() {
|
|
1227
|
+
themeSyncDispose?.();
|
|
1228
|
+
themeSyncDispose = void 0;
|
|
970
1229
|
root?.remove();
|
|
971
1230
|
root = void 0;
|
|
972
1231
|
frame = void 0;
|
|
@@ -992,6 +1251,7 @@ function ensurePopup() {
|
|
|
992
1251
|
frame.className = "dsh-tw-editor-frame";
|
|
993
1252
|
frame.setAttribute("sandbox", "allow-scripts allow-same-origin allow-forms allow-popups");
|
|
994
1253
|
frame.title = "TiddlyWiki 编辑器";
|
|
1254
|
+
themeSyncDispose = attachThemeSync(frame);
|
|
995
1255
|
const resize = document.createElement("div");
|
|
996
1256
|
resize.className = "dsh-tw-editor-resize";
|
|
997
1257
|
resize.title = "拖拽调整大小";
|
|
@@ -27745,6 +28005,8 @@ function renderConfigSection(body, config, refresh) {
|
|
|
27745
28005
|
checkField("ui.showQuickNote", "显示「知识库」按钮里的「快速笔记」入口", ui.showQuickNote !== false);
|
|
27746
28006
|
checkField("ui.showPanelStatus", "显示「知识库」按钮里的 TW 面板/重载入口与状态行", ui.showPanelStatus !== false);
|
|
27747
28007
|
checkField("ui.showSyncButton", "显示「知识库」按钮里的「同步」入口与 git 状态点", ui.showSyncButton !== false);
|
|
28008
|
+
checkField("ui.followDshTheme", "嵌入式 TW 跟随 DSH 深浅主题(暗色时自动切深色 palette,不写回 wiki)", ui.followDshTheme !== false);
|
|
28009
|
+
textField("ui.darkPalette", "暗色时 TW palette(tiddler 标题)", typeof ui.darkPalette === "string" && ui.darkPalette.length > 0 ? ui.darkPalette : "$:/palettes/CupertinoDark");
|
|
27748
28010
|
const save = make("button", "dsh-tw-settings-btn dsh-tw-settings-primary", "保存配置");
|
|
27749
28011
|
save.type = "button";
|
|
27750
28012
|
save.addEventListener("click", () => {
|
package/lib/index.js
CHANGED
|
@@ -949,10 +949,14 @@ function registerRoutes(ctx, deps) {
|
|
|
949
949
|
}
|
|
950
950
|
};
|
|
951
951
|
/**
|
|
952
|
-
* POST /dsh-tiddlywiki/agent/create — create (or adopt) one ordinary session
|
|
953
|
-
*
|
|
954
|
-
*
|
|
955
|
-
*
|
|
952
|
+
* POST /dsh-tiddlywiki/agent/create — create (or adopt) one ordinary session
|
|
953
|
+
* inside a real DSH workspace resolved from the requested path. The picker
|
|
954
|
+
* uses it for "new workspace / new session": the directory is materialised so
|
|
955
|
+
* a brand-new workspace actually exists on disk, the path is resolved to its
|
|
956
|
+
* (idempotent) Workspace, and the session is created with `workspaceId` so it
|
|
957
|
+
* lands under that workspace in the sidebar. Creating with bare `cwd` instead
|
|
958
|
+
* would leave the session in the ungrouped bucket even when its working
|
|
959
|
+
* directory matches an existing workspace path.
|
|
956
960
|
*/
|
|
957
961
|
const handleAgentCreate = async (req, res) => {
|
|
958
962
|
try {
|
|
@@ -984,10 +988,24 @@ function registerRoutes(ctx, deps) {
|
|
|
984
988
|
}, 503);
|
|
985
989
|
return;
|
|
986
990
|
}
|
|
991
|
+
const ws = deps.getWorkspaceRegistry();
|
|
987
992
|
if (cwd.length > 0) await mkdir(cwd, { recursive: true });
|
|
993
|
+
let created;
|
|
994
|
+
if (cwd.length > 0 && ws !== void 0) {
|
|
995
|
+
const workspace = await ws.create(cwd);
|
|
996
|
+
created = await sc.create({ workspaceId: workspace.id });
|
|
997
|
+
json$1(res, {
|
|
998
|
+
ok: true,
|
|
999
|
+
sessionId: created.sessionId,
|
|
1000
|
+
cwd,
|
|
1001
|
+
workspaceId: workspace.id
|
|
1002
|
+
});
|
|
1003
|
+
return;
|
|
1004
|
+
}
|
|
1005
|
+
created = await sc.create({ cwd: cwd.length > 0 ? cwd : void 0 });
|
|
988
1006
|
json$1(res, {
|
|
989
1007
|
ok: true,
|
|
990
|
-
sessionId:
|
|
1008
|
+
sessionId: created.sessionId,
|
|
991
1009
|
cwd: cwd || null
|
|
992
1010
|
});
|
|
993
1011
|
} catch (err) {
|
|
@@ -1500,6 +1518,11 @@ function registerRoutes(ctx, deps) {
|
|
|
1500
1518
|
//#region src/host/config.ts
|
|
1501
1519
|
/** Config tiddler (JSON string) where the settings page stores overrides. */
|
|
1502
1520
|
const CONFIG_TIDDLER = "$:/plugins/dsh-tiddlywiki/config";
|
|
1521
|
+
/**
|
|
1522
|
+
* Default dark palette the embedded TW switches to when DSH is dark
|
|
1523
|
+
* (mirrored in src/client/theme-sync.ts — the two bundles cannot share code).
|
|
1524
|
+
*/
|
|
1525
|
+
const DARK_PALETTE_DEFAULT = "$:/palettes/CupertinoDark";
|
|
1503
1526
|
function isPlainObject(value) {
|
|
1504
1527
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
1505
1528
|
}
|
|
@@ -2327,6 +2350,12 @@ function defineTool(options) {
|
|
|
2327
2350
|
*
|
|
2328
2351
|
* @module dsh-tiddlywiki/host/tools
|
|
2329
2352
|
*/
|
|
2353
|
+
/**
|
|
2354
|
+
* 约定标签:标记「由 Agent 撰写」的笔记。
|
|
2355
|
+
* 新建(title 不存在)时由 tiddlywiki_put / tiddlywiki_batch_put 自动补打;
|
|
2356
|
+
* 首页据此把这类笔记单独列在「Agent 区块」并从主标签列表排除。
|
|
2357
|
+
*/
|
|
2358
|
+
const AGENT_WRITTEN_TAG = "agent-written";
|
|
2330
2359
|
function snippetOf(text, max = 160) {
|
|
2331
2360
|
const flat = text.replace(/\s+/g, " ").trim();
|
|
2332
2361
|
return flat.length <= max ? flat : `${flat.slice(0, max)}…`;
|
|
@@ -2376,6 +2405,20 @@ function rewriteRefs(text, oldTitle, newTitle) {
|
|
|
2376
2405
|
count
|
|
2377
2406
|
};
|
|
2378
2407
|
}
|
|
2408
|
+
/**
|
|
2409
|
+
* Compute the final tags for a write:
|
|
2410
|
+
* - NEW tiddler (created by this tool) → auto-append the agent-written tag,
|
|
2411
|
+
* unless it is a `$:/` system tiddler (config/language/theme internals).
|
|
2412
|
+
* - Existing tiddler → keep the caller's tags as-is (an agent maintaining a
|
|
2413
|
+
* human note must NOT silently claim authorship; an already agent-written
|
|
2414
|
+
* note keeps its tag because the caller provides the full tag list).
|
|
2415
|
+
*/
|
|
2416
|
+
function finalTagsForWrite(title, existing, tags) {
|
|
2417
|
+
if (existing !== void 0) return tags;
|
|
2418
|
+
if (title.startsWith("$:/")) return tags;
|
|
2419
|
+
if (tags.includes("agent-written")) return tags;
|
|
2420
|
+
return [...tags, AGENT_WRITTEN_TAG];
|
|
2421
|
+
}
|
|
2379
2422
|
function registerTiddlywikiTools(ctx, deps) {
|
|
2380
2423
|
const disposers = [];
|
|
2381
2424
|
const register = (tool) => {
|
|
@@ -2584,7 +2627,7 @@ function registerTiddlywikiTools(ctx, deps) {
|
|
|
2584
2627
|
}));
|
|
2585
2628
|
register(defineTool({
|
|
2586
2629
|
name: "tiddlywiki_put",
|
|
2587
|
-
description: "写入(新建或覆盖)一个 TiddlyWiki tiddler。同名覆盖;tags 为标签数组,fields 为附加自定义字段(json 对象,会写入 tiddler 字段)。写入后触发自动 commit
|
|
2630
|
+
description: "写入(新建或覆盖)一个 TiddlyWiki tiddler。同名覆盖;tags 为标签数组,fields 为附加自定义字段(json 对象,会写入 tiddler 字段)。写入后触发自动 commit。新建(title 不存在)时自动补打 agent-written 标签标记「由 Agent 撰写」,无需手动添加。",
|
|
2588
2631
|
parameters: {
|
|
2589
2632
|
title: {
|
|
2590
2633
|
type: "string",
|
|
@@ -2624,25 +2667,27 @@ function registerTiddlywikiTools(ctx, deps) {
|
|
|
2624
2667
|
execute: async (args) => {
|
|
2625
2668
|
const wiki = deps.wiki();
|
|
2626
2669
|
if (wiki === void 0) throw new Error("TiddlyWiki 服务未运行(tiddlywiki_status 可查)");
|
|
2670
|
+
const existing = await wiki.get(args.title).catch(() => void 0);
|
|
2671
|
+
const tags = finalTagsForWrite(args.title, existing, Array.isArray(args.tags) ? args.tags.filter((t) => typeof t === "string" && t.trim().length > 0) : []);
|
|
2627
2672
|
const tiddler = {
|
|
2628
2673
|
title: args.title,
|
|
2629
2674
|
text: args.text
|
|
2630
2675
|
};
|
|
2631
|
-
if (
|
|
2676
|
+
if (tags.length > 0) tiddler.tags = tags;
|
|
2632
2677
|
if (args.fields !== void 0 && typeof args.fields === "object" && args.fields !== null) Object.assign(tiddler, args.fields);
|
|
2633
2678
|
await wiki.put(tiddler);
|
|
2634
2679
|
deps.autoCommit();
|
|
2635
2680
|
return {
|
|
2636
2681
|
ok: true,
|
|
2637
2682
|
title: args.title,
|
|
2638
|
-
tags
|
|
2683
|
+
tags,
|
|
2639
2684
|
fields: args.fields ?? null
|
|
2640
2685
|
};
|
|
2641
2686
|
}
|
|
2642
2687
|
}));
|
|
2643
2688
|
register(defineTool({
|
|
2644
2689
|
name: "tiddlywiki_batch_put",
|
|
2645
|
-
description: "批量写入/覆盖多个 TiddlyWiki tiddler(一次工具调用)。overwrite=false 时跳过已存在的标题;返回逐条结果。写入后触发自动 commit
|
|
2690
|
+
description: "批量写入/覆盖多个 TiddlyWiki tiddler(一次工具调用)。overwrite=false 时跳过已存在的标题;返回逐条结果。写入后触发自动 commit。新建(title 不存在)的条目会自动补打 agent-written 标签,无需手动添加。",
|
|
2646
2691
|
parameters: {
|
|
2647
2692
|
items: {
|
|
2648
2693
|
type: "array",
|
|
@@ -2706,22 +2751,22 @@ function registerTiddlywikiTools(ctx, deps) {
|
|
|
2706
2751
|
for (const item of list) {
|
|
2707
2752
|
if (typeof item.title !== "string" || item.title.length === 0) throw new Error("batch_put: 每条 items 都需要非空 title");
|
|
2708
2753
|
if (typeof item.text !== "string") throw new Error(`batch_put: items「${item.title}」缺少 text`);
|
|
2709
|
-
|
|
2710
|
-
|
|
2711
|
-
|
|
2712
|
-
|
|
2713
|
-
|
|
2714
|
-
|
|
2715
|
-
|
|
2716
|
-
|
|
2717
|
-
|
|
2718
|
-
}
|
|
2754
|
+
const existing = await wiki.get(item.title).catch(() => void 0);
|
|
2755
|
+
if (!overwrite && existing !== void 0) {
|
|
2756
|
+
skipped++;
|
|
2757
|
+
results.push({
|
|
2758
|
+
title: item.title,
|
|
2759
|
+
written: false,
|
|
2760
|
+
skipped: true
|
|
2761
|
+
});
|
|
2762
|
+
continue;
|
|
2719
2763
|
}
|
|
2764
|
+
const tags = finalTagsForWrite(item.title, existing, Array.isArray(item.tags) ? item.tags.filter((t) => typeof t === "string" && t.trim().length > 0) : []);
|
|
2720
2765
|
const tiddler = {
|
|
2721
2766
|
title: item.title,
|
|
2722
2767
|
text: item.text
|
|
2723
2768
|
};
|
|
2724
|
-
if (
|
|
2769
|
+
if (tags.length > 0) tiddler.tags = tags;
|
|
2725
2770
|
if (item.fields !== void 0 && typeof item.fields === "object" && item.fields !== null) Object.assign(tiddler, item.fields);
|
|
2726
2771
|
await wiki.put(tiddler);
|
|
2727
2772
|
written++;
|
|
@@ -3094,6 +3139,8 @@ const DEFAULTS = {
|
|
|
3094
3139
|
showQuickNote: true,
|
|
3095
3140
|
showPanelStatus: true,
|
|
3096
3141
|
showSyncButton: true,
|
|
3142
|
+
followDshTheme: true,
|
|
3143
|
+
darkPalette: DARK_PALETTE_DEFAULT,
|
|
3097
3144
|
sendToAgent: { enabled: true }
|
|
3098
3145
|
},
|
|
3099
3146
|
auth: {
|
|
@@ -3182,7 +3229,9 @@ pull 冲突后:先 \`tiddlywiki_git_resolve files=[冲突文件] strategy=keep
|
|
|
3182
3229
|
|
|
3183
3230
|
把 wiki 当作长期记忆与知识沉淀的地方:会议纪要、决策记录、调研笔记、随手的想法都可存成独立 tiddler(tag 建议用 inbox/meeting/decision 等便于检索)。
|
|
3184
3231
|
|
|
3185
|
-
用本插件自动创建笔记时,除了业务性 tag 外,请把「当前工作区(项目)的名字」也作为标签之一加上去(例如 \`tiddlywiki_put\` 的 tags 里带上当前 workspace
|
|
3232
|
+
用本插件自动创建笔记时,除了业务性 tag 外,请把「当前工作区(项目)的名字」也作为标签之一加上去(例如 \`tiddlywiki_put\` 的 tags 里带上当前 workspace 名),这样笔记能按项目归集、检索。
|
|
3233
|
+
|
|
3234
|
+
**Agent 笔记标签约定**:\`tiddlywiki_put\` / \`tiddlywiki_batch_put\` 新建笔记时,插件会自动补打 \`agent-written\` 标签(标记「由 Agent 撰写」),无需手动添加,也不要手动移除它(除非用户明确要求)。首页会把 Agent 笔记单独列在「Agent 区块」,主标签列表只统计人类笔记。若某篇 Agent 笔记后续被人类编辑过,请在该笔记上补打 \`human-edited\` 标签,首页会把它归入「Agent + 人工」档。覆盖写入已有的(人类)笔记时不会自动加 agent-written,请保持笔记原本的归属。`;
|
|
3186
3235
|
/**
|
|
3187
3236
|
* Mount the host half.
|
|
3188
3237
|
* @param ctx - the plugin context (tools + systemPrompt injected).
|
|
@@ -3222,11 +3271,17 @@ function apply(ctx, rawConfig = {}) {
|
|
|
3222
3271
|
const tag = eff().note?.tag;
|
|
3223
3272
|
return typeof tag === "string" && tag.trim().length > 0 ? tag : config.note.tag;
|
|
3224
3273
|
};
|
|
3225
|
-
const effectiveUi = () =>
|
|
3226
|
-
|
|
3227
|
-
|
|
3228
|
-
|
|
3229
|
-
|
|
3274
|
+
const effectiveUi = () => {
|
|
3275
|
+
const ui = eff().ui ?? {};
|
|
3276
|
+
const palette = typeof ui.darkPalette === "string" && ui.darkPalette.trim().length > 0 ? ui.darkPalette.trim() : DARK_PALETTE_DEFAULT;
|
|
3277
|
+
return {
|
|
3278
|
+
showQuickNote: ui.showQuickNote !== false,
|
|
3279
|
+
showPanelStatus: ui.showPanelStatus !== false,
|
|
3280
|
+
showSyncButton: ui.showSyncButton !== false,
|
|
3281
|
+
followDshTheme: ui.followDshTheme !== false,
|
|
3282
|
+
darkPalette: palette
|
|
3283
|
+
};
|
|
3284
|
+
};
|
|
3230
3285
|
const disposers = [];
|
|
3231
3286
|
const disposeAll = () => {
|
|
3232
3287
|
for (const dispose of disposers.splice(0)) dispose();
|
|
@@ -3335,6 +3390,7 @@ function apply(ctx, rawConfig = {}) {
|
|
|
3335
3390
|
ctx.inject(["webServer"], (webCtx) => {
|
|
3336
3391
|
const ws = webCtx.webServer;
|
|
3337
3392
|
const getSessionController = () => ctx.get("sessionController");
|
|
3393
|
+
const getWorkspaceRegistry = () => ctx.get("workspaceRegistry");
|
|
3338
3394
|
const disposeRoutes = registerRoutes({ webServer: ws }, {
|
|
3339
3395
|
server,
|
|
3340
3396
|
getClient: client,
|
|
@@ -3344,6 +3400,7 @@ function apply(ctx, rawConfig = {}) {
|
|
|
3344
3400
|
uiDefaults: () => effectiveUi(),
|
|
3345
3401
|
getWikiPath: () => wikiPath,
|
|
3346
3402
|
getSessionController,
|
|
3403
|
+
getWorkspaceRegistry,
|
|
3347
3404
|
sendToAgentEnabled: () => eff().ui?.sendToAgent?.enabled !== false,
|
|
3348
3405
|
sendToAgentToken: () => {
|
|
3349
3406
|
const token = eff().ui?.sendToAgent?.token;
|