dsh-tiddlywiki 0.26.5 → 0.26.6
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/LICENSE +21 -21
- package/README.md +2 -1
- package/cordis.patch.yml +13 -13
- package/lib/index.js +45 -14
- package/lib/index.js.map +1 -1
- package/package.json +1 -1
- package/src/client/markdown-editor.ts +128 -128
- package/src/client/state.ts +39 -39
- package/src/client/toast.ts +22 -22
- package/src/host/tools.ts +56 -15
- package/docs/code-review-2026-09-17.md +0 -151
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-tiddlywiki",
|
|
3
3
|
"description": "TiddlyWiki 5 as the DSH persistent knowledge base: fifteen tiddlywiki_* agent tools (filtered search/recent/tags/batch/rewrite/trash/lint + git sync & conflict resolve), a full TiddlyWiki editor embedded in the GUI center column (same-origin proxy, works over Tailscale/LAN/domain), a right-side wiki tab, a per-session summary tab, a quick-note card with draft auto-save behind a single knowledge FAB, an optional clip bookmarklet that also saves selected images as attachments, and git-based sync with auto-commit. Mounts via the official dsh plugin system — no DSH source changes.",
|
|
4
|
-
"version": "0.26.
|
|
4
|
+
"version": "0.26.6",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"keywords": [
|
|
7
7
|
"dsh",
|
|
@@ -1,128 +1,128 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* CodeMirror 6 Markdown editor for the quick-note widget (replaces the
|
|
3
|
-
* hand-rolled regex highlighter in the old markdown.ts — real Lezer syntax
|
|
4
|
-
* tree, proper GFM coverage, undo/redo, and a native editing surface).
|
|
5
|
-
*
|
|
6
|
-
* The widget stays a plain-DOM product: CodeMirror is framework-agnostic, so
|
|
7
|
-
* no React is pulled in. The editor is built into a wrapper `<div>`
|
|
8
|
-
* (`.dsh-tw-note-editor`) that the widget can append, drag-drop onto, and
|
|
9
|
-
* re-read via `getValue()`/`setValue()`.
|
|
10
|
-
*
|
|
11
|
-
* Theming follows the DSH design tokens (`--dsw-alias-*`) through a
|
|
12
|
-
* `HighlightStyle` that maps Markdown tags onto the same palette the old
|
|
13
|
-
* `.md-*` rules used, so light/dark both look right.
|
|
14
|
-
*
|
|
15
|
-
* @module dsh-tiddlywiki/client/markdown-editor
|
|
16
|
-
*/
|
|
17
|
-
import { EditorView, keymap, placeholder, drawSelection } from '@codemirror/view'
|
|
18
|
-
import { EditorState } from '@codemirror/state'
|
|
19
|
-
import { markdown, markdownLanguage } from '@codemirror/lang-markdown'
|
|
20
|
-
import { syntaxHighlighting, HighlightStyle } from '@codemirror/language'
|
|
21
|
-
import { defaultKeymap, history, historyKeymap } from '@codemirror/commands'
|
|
22
|
-
import { tags as t } from '@lezer/highlight'
|
|
23
|
-
|
|
24
|
-
/** Design-token aliases (same values as styles.ts / the old .md-* rules). */
|
|
25
|
-
const BRAND = 'var(--dsw-alias-brand-primary, #3e63dd)'
|
|
26
|
-
const BRAND_DIM = 'color-mix(in srgb, var(--dsw-alias-brand-primary, #3e63dd) 55%, transparent)'
|
|
27
|
-
const SECONDARY = 'var(--dsw-alias-label-secondary, #888)'
|
|
28
|
-
const DIMMED = 'var(--dsw-alias-label-dimmed, #999)'
|
|
29
|
-
const MONO = 'ui-monospace, "Cascadia Mono", Consolas, "SF Mono", Menlo, monospace'
|
|
30
|
-
const CODE_BG = 'color-mix(in srgb, var(--dsw-alias-label-secondary, #888) 12%, transparent)'
|
|
31
|
-
const CODE_BLOCK_BG = 'color-mix(in srgb, var(--dsw-alias-label-secondary, #888) 8%, transparent)'
|
|
32
|
-
|
|
33
|
-
/** Markdown token → design-token style map (mirrors the old .md-* palette). */
|
|
34
|
-
const mdHighlight = HighlightStyle.define([
|
|
35
|
-
// Headings (# … ######) — brand + bold.
|
|
36
|
-
{ tag: [t.heading1, t.heading2, t.heading3, t.heading4, t.heading5, t.heading6], color: BRAND, fontWeight: '700' },
|
|
37
|
-
// Markers: #, >, -, *, `, [ ] etc.
|
|
38
|
-
{ tag: t.processingInstruction, color: BRAND_DIM },
|
|
39
|
-
// Inline code + fenced code text — monospace, tinted background.
|
|
40
|
-
{ tag: t.monospace, fontFamily: MONO, backgroundColor: CODE_BG, borderRadius: '4px', padding: '0 3px' },
|
|
41
|
-
// Code block content (inside a fence) gets a slightly wider tint.
|
|
42
|
-
{ tag: t.content, fontFamily: MONO, backgroundColor: CODE_BLOCK_BG },
|
|
43
|
-
{ tag: t.strong, fontWeight: '700' },
|
|
44
|
-
{ tag: t.emphasis, fontStyle: 'italic' },
|
|
45
|
-
{ tag: t.strikethrough, textDecoration: 'line-through', opacity: '.75' },
|
|
46
|
-
{ tag: t.link, color: BRAND, textDecoration: 'underline' },
|
|
47
|
-
{ tag: t.url, color: BRAND_DIM, textDecoration: 'underline dotted' },
|
|
48
|
-
{ tag: t.quote, fontStyle: 'italic', color: SECONDARY },
|
|
49
|
-
{ tag: t.contentSeparator, color: DIMMED, textDecoration: 'line-through' },
|
|
50
|
-
{ tag: t.comment, color: DIMMED },
|
|
51
|
-
])
|
|
52
|
-
|
|
53
|
-
/** Public editor surface the quick-note widget consumes. */
|
|
54
|
-
export interface MarkdownEditor {
|
|
55
|
-
/** Wrapper element (append/drag-target). */
|
|
56
|
-
el: HTMLDivElement
|
|
57
|
-
/** The CodeMirror EditorView (advanced use / future extensions). */
|
|
58
|
-
view: EditorView
|
|
59
|
-
getValue(): string
|
|
60
|
-
setValue(value: string): void
|
|
61
|
-
/** Insert a Markdown line at the current caret (used by file upload). */
|
|
62
|
-
insertAtCaret(markdown: string): void
|
|
63
|
-
focus(): void
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
export interface MarkdownEditorOptions {
|
|
67
|
-
/** Placeholder text shown when the doc is empty. */
|
|
68
|
-
placeholder?: string
|
|
69
|
-
/** Called on Ctrl/Cmd+Enter (wired after save is defined by the widget). */
|
|
70
|
-
onSave?: () => void
|
|
71
|
-
/** Called after every doc change (used for draft auto-save). */
|
|
72
|
-
onChange?: () => void
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
/**
|
|
76
|
-
* Build a CodeMirror 6 Markdown editor inside `.dsh-tw-note-editor`.
|
|
77
|
-
* GFM base language (strikethrough, tables, task lists, autolinks) — strictly
|
|
78
|
-
* more coverage than the old regex highlighter.
|
|
79
|
-
*/
|
|
80
|
-
export function buildMarkdownEditor(opts: MarkdownEditorOptions = {}): MarkdownEditor {
|
|
81
|
-
const wrap = document.createElement('div')
|
|
82
|
-
wrap.className = 'dsh-tw-note-editor'
|
|
83
|
-
|
|
84
|
-
const view = new EditorView({
|
|
85
|
-
parent: wrap,
|
|
86
|
-
state: EditorState.create({
|
|
87
|
-
doc: '',
|
|
88
|
-
extensions: [
|
|
89
|
-
EditorView.lineWrapping,
|
|
90
|
-
history(),
|
|
91
|
-
markdown({ base: markdownLanguage }),
|
|
92
|
-
syntaxHighlighting(mdHighlight),
|
|
93
|
-
drawSelection(),
|
|
94
|
-
placeholder(opts.placeholder ?? ''),
|
|
95
|
-
keymap.of([
|
|
96
|
-
...defaultKeymap,
|
|
97
|
-
...historyKeymap,
|
|
98
|
-
{ key: 'Mod-Enter', run: () => { opts.onSave?.(); return true } },
|
|
99
|
-
]),
|
|
100
|
-
EditorView.updateListener.of((update) => {
|
|
101
|
-
if (update.docChanged) opts.onChange?.()
|
|
102
|
-
}),
|
|
103
|
-
],
|
|
104
|
-
}),
|
|
105
|
-
})
|
|
106
|
-
|
|
107
|
-
return {
|
|
108
|
-
el: wrap,
|
|
109
|
-
view,
|
|
110
|
-
getValue: () => view.state.doc.toString(),
|
|
111
|
-
setValue(value: string) {
|
|
112
|
-
view.dispatch({ changes: { from: 0, to: view.state.doc.length, insert: value } })
|
|
113
|
-
},
|
|
114
|
-
insertAtCaret(markdownLine: string) {
|
|
115
|
-
const { from } = view.state.selection.main
|
|
116
|
-
const line = view.state.doc.lineAt(from)
|
|
117
|
-
const atLineStart = from === line.from
|
|
118
|
-
const insert = `${atLineStart ? '' : '\n'}${markdownLine}\n`
|
|
119
|
-
view.dispatch({
|
|
120
|
-
changes: { from, insert },
|
|
121
|
-
selection: { anchor: from + insert.length },
|
|
122
|
-
scrollIntoView: true,
|
|
123
|
-
})
|
|
124
|
-
view.focus()
|
|
125
|
-
},
|
|
126
|
-
focus: () => view.focus(),
|
|
127
|
-
}
|
|
128
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* CodeMirror 6 Markdown editor for the quick-note widget (replaces the
|
|
3
|
+
* hand-rolled regex highlighter in the old markdown.ts — real Lezer syntax
|
|
4
|
+
* tree, proper GFM coverage, undo/redo, and a native editing surface).
|
|
5
|
+
*
|
|
6
|
+
* The widget stays a plain-DOM product: CodeMirror is framework-agnostic, so
|
|
7
|
+
* no React is pulled in. The editor is built into a wrapper `<div>`
|
|
8
|
+
* (`.dsh-tw-note-editor`) that the widget can append, drag-drop onto, and
|
|
9
|
+
* re-read via `getValue()`/`setValue()`.
|
|
10
|
+
*
|
|
11
|
+
* Theming follows the DSH design tokens (`--dsw-alias-*`) through a
|
|
12
|
+
* `HighlightStyle` that maps Markdown tags onto the same palette the old
|
|
13
|
+
* `.md-*` rules used, so light/dark both look right.
|
|
14
|
+
*
|
|
15
|
+
* @module dsh-tiddlywiki/client/markdown-editor
|
|
16
|
+
*/
|
|
17
|
+
import { EditorView, keymap, placeholder, drawSelection } from '@codemirror/view'
|
|
18
|
+
import { EditorState } from '@codemirror/state'
|
|
19
|
+
import { markdown, markdownLanguage } from '@codemirror/lang-markdown'
|
|
20
|
+
import { syntaxHighlighting, HighlightStyle } from '@codemirror/language'
|
|
21
|
+
import { defaultKeymap, history, historyKeymap } from '@codemirror/commands'
|
|
22
|
+
import { tags as t } from '@lezer/highlight'
|
|
23
|
+
|
|
24
|
+
/** Design-token aliases (same values as styles.ts / the old .md-* rules). */
|
|
25
|
+
const BRAND = 'var(--dsw-alias-brand-primary, #3e63dd)'
|
|
26
|
+
const BRAND_DIM = 'color-mix(in srgb, var(--dsw-alias-brand-primary, #3e63dd) 55%, transparent)'
|
|
27
|
+
const SECONDARY = 'var(--dsw-alias-label-secondary, #888)'
|
|
28
|
+
const DIMMED = 'var(--dsw-alias-label-dimmed, #999)'
|
|
29
|
+
const MONO = 'ui-monospace, "Cascadia Mono", Consolas, "SF Mono", Menlo, monospace'
|
|
30
|
+
const CODE_BG = 'color-mix(in srgb, var(--dsw-alias-label-secondary, #888) 12%, transparent)'
|
|
31
|
+
const CODE_BLOCK_BG = 'color-mix(in srgb, var(--dsw-alias-label-secondary, #888) 8%, transparent)'
|
|
32
|
+
|
|
33
|
+
/** Markdown token → design-token style map (mirrors the old .md-* palette). */
|
|
34
|
+
const mdHighlight = HighlightStyle.define([
|
|
35
|
+
// Headings (# … ######) — brand + bold.
|
|
36
|
+
{ tag: [t.heading1, t.heading2, t.heading3, t.heading4, t.heading5, t.heading6], color: BRAND, fontWeight: '700' },
|
|
37
|
+
// Markers: #, >, -, *, `, [ ] etc.
|
|
38
|
+
{ tag: t.processingInstruction, color: BRAND_DIM },
|
|
39
|
+
// Inline code + fenced code text — monospace, tinted background.
|
|
40
|
+
{ tag: t.monospace, fontFamily: MONO, backgroundColor: CODE_BG, borderRadius: '4px', padding: '0 3px' },
|
|
41
|
+
// Code block content (inside a fence) gets a slightly wider tint.
|
|
42
|
+
{ tag: t.content, fontFamily: MONO, backgroundColor: CODE_BLOCK_BG },
|
|
43
|
+
{ tag: t.strong, fontWeight: '700' },
|
|
44
|
+
{ tag: t.emphasis, fontStyle: 'italic' },
|
|
45
|
+
{ tag: t.strikethrough, textDecoration: 'line-through', opacity: '.75' },
|
|
46
|
+
{ tag: t.link, color: BRAND, textDecoration: 'underline' },
|
|
47
|
+
{ tag: t.url, color: BRAND_DIM, textDecoration: 'underline dotted' },
|
|
48
|
+
{ tag: t.quote, fontStyle: 'italic', color: SECONDARY },
|
|
49
|
+
{ tag: t.contentSeparator, color: DIMMED, textDecoration: 'line-through' },
|
|
50
|
+
{ tag: t.comment, color: DIMMED },
|
|
51
|
+
])
|
|
52
|
+
|
|
53
|
+
/** Public editor surface the quick-note widget consumes. */
|
|
54
|
+
export interface MarkdownEditor {
|
|
55
|
+
/** Wrapper element (append/drag-target). */
|
|
56
|
+
el: HTMLDivElement
|
|
57
|
+
/** The CodeMirror EditorView (advanced use / future extensions). */
|
|
58
|
+
view: EditorView
|
|
59
|
+
getValue(): string
|
|
60
|
+
setValue(value: string): void
|
|
61
|
+
/** Insert a Markdown line at the current caret (used by file upload). */
|
|
62
|
+
insertAtCaret(markdown: string): void
|
|
63
|
+
focus(): void
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export interface MarkdownEditorOptions {
|
|
67
|
+
/** Placeholder text shown when the doc is empty. */
|
|
68
|
+
placeholder?: string
|
|
69
|
+
/** Called on Ctrl/Cmd+Enter (wired after save is defined by the widget). */
|
|
70
|
+
onSave?: () => void
|
|
71
|
+
/** Called after every doc change (used for draft auto-save). */
|
|
72
|
+
onChange?: () => void
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Build a CodeMirror 6 Markdown editor inside `.dsh-tw-note-editor`.
|
|
77
|
+
* GFM base language (strikethrough, tables, task lists, autolinks) — strictly
|
|
78
|
+
* more coverage than the old regex highlighter.
|
|
79
|
+
*/
|
|
80
|
+
export function buildMarkdownEditor(opts: MarkdownEditorOptions = {}): MarkdownEditor {
|
|
81
|
+
const wrap = document.createElement('div')
|
|
82
|
+
wrap.className = 'dsh-tw-note-editor'
|
|
83
|
+
|
|
84
|
+
const view = new EditorView({
|
|
85
|
+
parent: wrap,
|
|
86
|
+
state: EditorState.create({
|
|
87
|
+
doc: '',
|
|
88
|
+
extensions: [
|
|
89
|
+
EditorView.lineWrapping,
|
|
90
|
+
history(),
|
|
91
|
+
markdown({ base: markdownLanguage }),
|
|
92
|
+
syntaxHighlighting(mdHighlight),
|
|
93
|
+
drawSelection(),
|
|
94
|
+
placeholder(opts.placeholder ?? ''),
|
|
95
|
+
keymap.of([
|
|
96
|
+
...defaultKeymap,
|
|
97
|
+
...historyKeymap,
|
|
98
|
+
{ key: 'Mod-Enter', run: () => { opts.onSave?.(); return true } },
|
|
99
|
+
]),
|
|
100
|
+
EditorView.updateListener.of((update) => {
|
|
101
|
+
if (update.docChanged) opts.onChange?.()
|
|
102
|
+
}),
|
|
103
|
+
],
|
|
104
|
+
}),
|
|
105
|
+
})
|
|
106
|
+
|
|
107
|
+
return {
|
|
108
|
+
el: wrap,
|
|
109
|
+
view,
|
|
110
|
+
getValue: () => view.state.doc.toString(),
|
|
111
|
+
setValue(value: string) {
|
|
112
|
+
view.dispatch({ changes: { from: 0, to: view.state.doc.length, insert: value } })
|
|
113
|
+
},
|
|
114
|
+
insertAtCaret(markdownLine: string) {
|
|
115
|
+
const { from } = view.state.selection.main
|
|
116
|
+
const line = view.state.doc.lineAt(from)
|
|
117
|
+
const atLineStart = from === line.from
|
|
118
|
+
const insert = `${atLineStart ? '' : '\n'}${markdownLine}\n`
|
|
119
|
+
view.dispatch({
|
|
120
|
+
changes: { from, insert },
|
|
121
|
+
selection: { anchor: from + insert.length },
|
|
122
|
+
scrollIntoView: true,
|
|
123
|
+
})
|
|
124
|
+
view.focus()
|
|
125
|
+
},
|
|
126
|
+
focus: () => view.focus(),
|
|
127
|
+
}
|
|
128
|
+
}
|
package/src/client/state.ts
CHANGED
|
@@ -1,39 +1,39 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Tiny pub/sub panel state shared by the sidebar entry and the center panel.
|
|
3
|
-
*
|
|
4
|
-
* @module dsh-tiddlywiki/client/state
|
|
5
|
-
*/
|
|
6
|
-
|
|
7
|
-
export type PanelListener = (open: boolean) => void
|
|
8
|
-
|
|
9
|
-
export class PanelState {
|
|
10
|
-
private open = false
|
|
11
|
-
private readonly listeners = new Set<PanelListener>()
|
|
12
|
-
|
|
13
|
-
isOpen(): boolean {
|
|
14
|
-
return this.open
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
toggle(): void {
|
|
18
|
-
this.set(!this.open)
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
openPanel(): void {
|
|
22
|
-
this.set(true)
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
closePanel(): void {
|
|
26
|
-
this.set(false)
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
set(value: boolean): void {
|
|
30
|
-
if (this.open === value) return
|
|
31
|
-
this.open = value
|
|
32
|
-
for (const listener of [...this.listeners]) listener(value)
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
subscribe(listener: PanelListener): () => void {
|
|
36
|
-
this.listeners.add(listener)
|
|
37
|
-
return () => { this.listeners.delete(listener) }
|
|
38
|
-
}
|
|
39
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* Tiny pub/sub panel state shared by the sidebar entry and the center panel.
|
|
3
|
+
*
|
|
4
|
+
* @module dsh-tiddlywiki/client/state
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
export type PanelListener = (open: boolean) => void
|
|
8
|
+
|
|
9
|
+
export class PanelState {
|
|
10
|
+
private open = false
|
|
11
|
+
private readonly listeners = new Set<PanelListener>()
|
|
12
|
+
|
|
13
|
+
isOpen(): boolean {
|
|
14
|
+
return this.open
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
toggle(): void {
|
|
18
|
+
this.set(!this.open)
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
openPanel(): void {
|
|
22
|
+
this.set(true)
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
closePanel(): void {
|
|
26
|
+
this.set(false)
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
set(value: boolean): void {
|
|
30
|
+
if (this.open === value) return
|
|
31
|
+
this.open = value
|
|
32
|
+
for (const listener of [...this.listeners]) listener(value)
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
subscribe(listener: PanelListener): () => void {
|
|
36
|
+
this.listeners.add(listener)
|
|
37
|
+
return () => { this.listeners.delete(listener) }
|
|
38
|
+
}
|
|
39
|
+
}
|
package/src/client/toast.ts
CHANGED
|
@@ -1,22 +1,22 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* One-shot transient toast, shared by the quick-note widget and the settings
|
|
3
|
-
* page. Reuses a single body-level element (`.dsh-tw-toast`) with a show
|
|
4
|
-
* class; the element is created lazily and styled by styles.ts.
|
|
5
|
-
*
|
|
6
|
-
* @module dsh-tiddlywiki/client/toast
|
|
7
|
-
*/
|
|
8
|
-
|
|
9
|
-
export function toast(message: string): void {
|
|
10
|
-
let el = document.querySelector<HTMLElement>('.dsh-tw-toast')
|
|
11
|
-
if (el === null) {
|
|
12
|
-
el = document.createElement('div')
|
|
13
|
-
el.className = 'dsh-tw-toast'
|
|
14
|
-
document.body.append(el)
|
|
15
|
-
}
|
|
16
|
-
el.textContent = message
|
|
17
|
-
el.classList.add('dsh-tw-toast-show')
|
|
18
|
-
clearTimeout((el as HTMLElement & { __twToastTimer?: number }).__twToastTimer)
|
|
19
|
-
;(el as HTMLElement & { __twToastTimer?: number }).__twToastTimer = window.setTimeout(() => {
|
|
20
|
-
el?.classList.remove('dsh-tw-toast-show')
|
|
21
|
-
}, 2_500)
|
|
22
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* One-shot transient toast, shared by the quick-note widget and the settings
|
|
3
|
+
* page. Reuses a single body-level element (`.dsh-tw-toast`) with a show
|
|
4
|
+
* class; the element is created lazily and styled by styles.ts.
|
|
5
|
+
*
|
|
6
|
+
* @module dsh-tiddlywiki/client/toast
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
export function toast(message: string): void {
|
|
10
|
+
let el = document.querySelector<HTMLElement>('.dsh-tw-toast')
|
|
11
|
+
if (el === null) {
|
|
12
|
+
el = document.createElement('div')
|
|
13
|
+
el.className = 'dsh-tw-toast'
|
|
14
|
+
document.body.append(el)
|
|
15
|
+
}
|
|
16
|
+
el.textContent = message
|
|
17
|
+
el.classList.add('dsh-tw-toast-show')
|
|
18
|
+
clearTimeout((el as HTMLElement & { __twToastTimer?: number }).__twToastTimer)
|
|
19
|
+
;(el as HTMLElement & { __twToastTimer?: number }).__twToastTimer = window.setTimeout(() => {
|
|
20
|
+
el?.classList.remove('dsh-tw-toast-show')
|
|
21
|
+
}, 2_500)
|
|
22
|
+
}
|
package/src/host/tools.ts
CHANGED
|
@@ -392,23 +392,47 @@ function rewriteRefs(text: string, oldTitle: string, newTitle: string): { text:
|
|
|
392
392
|
}
|
|
393
393
|
|
|
394
394
|
/**
|
|
395
|
-
* Insert `addition`
|
|
396
|
-
*
|
|
397
|
-
*
|
|
398
|
-
*
|
|
395
|
+
* Insert `addition` into the section introduced by `heading` (Markdown `#`/`##`…
|
|
396
|
+
* or wikitext `!`/`!!`…). Used by tiddlywiki_append so an agent can add to one
|
|
397
|
+
* section of a long note without rewriting the whole file.
|
|
398
|
+
*
|
|
399
|
+
* **落点语义**:插在「该标题之后、**下一个任意级别的标题**之前」。所以当标题后面
|
|
400
|
+
* 紧跟子标题时(`## 4. 定时任务` + `### 槽位表`),内容落在两者**之间**,也就是
|
|
401
|
+
* 「章节开头」而不是「整节末尾」——这一点以前只在 schema 里写成「段落的末尾」,
|
|
402
|
+
* 容易让调用方误会(v0.26.6 起描述与回执都写清楚了)。
|
|
403
|
+
*
|
|
404
|
+
* **返回 `matched`**:`false` = 没找到该标题,文本被追加到**文末**。调用方**必须**
|
|
405
|
+
* 把它透出去(v0.26.6 之前这个回退是静默的,回执照样打「段落「X」」,
|
|
406
|
+
* 于是一次定位失败看起来像成功——Agent 没有任何办法自检)。
|
|
407
|
+
*
|
|
408
|
+
* **CRLF(v0.26.6 修复)**:tiddler 正文可能是 CRLF(Windows 上写的/同步来的笔记),
|
|
409
|
+
* 而 `split('\n')` 会让每行结尾都留着 `\r`。JS 里 `(.*)` 不匹配 `\r`、`$` 也不匹配
|
|
410
|
+
* `\r` 之前的位置,于是 `/^#{1,6}\s+(.*)$/` 对**每一个**标题都匹配失败 ⇒ 所有带
|
|
411
|
+
* `heading` 的 append 都静默落到了文末。**只在 CRLF 笔记上复现**(纯 LF 笔记一切正常),
|
|
412
|
+
* 这就是它像「偶发」的原因。现在只在**匹配时**剥掉行尾 CR,原文行尾一个字节都不动;
|
|
413
|
+
* 并且新块用**文档自己的行尾**写入,避免 CRLF 笔记被越写越乱(混入 LF 块)。
|
|
399
414
|
*/
|
|
400
|
-
function insertIntoSection(base: string, heading: string, addition: string): string {
|
|
415
|
+
function insertIntoSection(base: string, heading: string, addition: string): { text: string; matched: boolean } {
|
|
401
416
|
const lines = base.split('\n')
|
|
402
417
|
const headingText = (line: string): string | null => {
|
|
403
|
-
|
|
418
|
+
// 只在匹配时剥离行尾 CR:CRLF 笔记的每一行都会带上它。
|
|
419
|
+
const s = line.replace(/\r$/, '')
|
|
420
|
+
const md = /^#{1,6}\s+(.*)$/.exec(s)
|
|
404
421
|
if (md !== null) return (md[1] ?? '').trim()
|
|
405
|
-
const tw = /^!{1,6}\s*(.*)$/.exec(
|
|
422
|
+
const tw = /^!{1,6}\s*(.*)$/.exec(s)
|
|
406
423
|
if (tw !== null) return (tw[1] ?? '').trim()
|
|
407
424
|
return null
|
|
408
425
|
}
|
|
409
426
|
const start = lines.findIndex((line) => headingText(line) === heading)
|
|
427
|
+
// 新块跟随文档主流行尾:CRLF 文档里不要再塞 LF 块。
|
|
428
|
+
const eol = base.includes('\r\n') ? '\r\n' : '\n'
|
|
429
|
+
const adapt = (s: string): string => (eol === '\n' ? s.replace(/\r\n/g, '\n') : s.replace(/\r\n/g, '\n').split('\n').join('\r\n'))
|
|
430
|
+
const block = adapt(addition)
|
|
410
431
|
if (start < 0) {
|
|
411
|
-
return
|
|
432
|
+
return {
|
|
433
|
+
text: base.trim().length === 0 ? block : `${base.replace(/\s+$/, '')}${eol}${eol}${block}`,
|
|
434
|
+
matched: false,
|
|
435
|
+
}
|
|
412
436
|
}
|
|
413
437
|
let end = lines.length
|
|
414
438
|
for (let i = start + 1; i < lines.length; i++) {
|
|
@@ -419,8 +443,11 @@ function insertIntoSection(base: string, heading: string, addition: string): str
|
|
|
419
443
|
}
|
|
420
444
|
const before = lines.slice(0, end).join('\n').replace(/\s+$/, '')
|
|
421
445
|
const after = lines.slice(end).join('\n')
|
|
422
|
-
const head = `${before}
|
|
423
|
-
return
|
|
446
|
+
const head = `${before}${eol}${eol}${block}`
|
|
447
|
+
return {
|
|
448
|
+
text: after.trim().length === 0 ? head : `${head}${eol}${eol}${after.replace(/^\s+/, '')}`,
|
|
449
|
+
matched: true,
|
|
450
|
+
}
|
|
424
451
|
}
|
|
425
452
|
|
|
426
453
|
|
|
@@ -1100,12 +1127,12 @@ export function registerTiddlywikiTools(ctx: ToolsCtx, deps: ToolsDeps): Array<(
|
|
|
1100
1127
|
// ── tiddlywiki_append ────────────────────────────────────────────────────
|
|
1101
1128
|
register(defineTool({
|
|
1102
1129
|
name: 'tiddlywiki_append',
|
|
1103
|
-
description: '向已有 tiddler 追加/前插文本,或写入指定标题段落的末尾(无需先读全文、不会整篇覆盖)——适合日志、批注、清单的增量写入。条目不存在时默认新建(createIfMissing=false 则报错)。**写入既有条目走与 tiddlywiki_put 同一套写策略**:原有 tags、自定义字段与**内容类型**全部保留(不会把 Markdown 笔记改成 wikitext、也不会把 CSS 改成 Markdown);可用 fields 显式覆盖字段/类型。新建条目未指定内容类型时才默认 text/markdown(并像 tiddlywiki_put 一样自动补 agent-written 与 `ws/<项目名>` 工作区标记)。⚠️ 本工具内部是「读全文 → 整篇回写」,属于覆盖写入:改前请先 `tiddlywiki_get`,并把读到的 `modified` 作为 `expectedModified` 传回(v0.25.0 起支持),否则人类在 TW
|
|
1130
|
+
description: '向已有 tiddler 追加/前插文本,或写入指定标题段落的末尾(无需先读全文、不会整篇覆盖)——适合日志、批注、清单的增量写入。条目不存在时默认新建(createIfMissing=false 则报错)。**写入既有条目走与 tiddlywiki_put 同一套写策略**:原有 tags、自定义字段与**内容类型**全部保留(不会把 Markdown 笔记改成 wikitext、也不会把 CSS 改成 Markdown);可用 fields 显式覆盖字段/类型。新建条目未指定内容类型时才默认 text/markdown(并像 tiddlywiki_put 一样自动补 agent-written 与 `ws/<项目名>` 工作区标记)。⚠️ 本工具内部是「读全文 → 整篇回写」,属于覆盖写入:改前请先 `tiddlywiki_get`,并把读到的 `modified` 作为 `expectedModified` 传回(v0.25.0 起支持),否则人类在 TW 编辑器里的并发修改会被静默回滚。带 `heading` 时返回 `headingMatched`:`false` = 没找到该标题、文本已追加到**文末**(CRLF 行尾的笔记也能正确定位)。',
|
|
1104
1131
|
parameters: {
|
|
1105
1132
|
title: { type: 'string', description: 'tiddler 标题', required: true },
|
|
1106
1133
|
text: { type: 'string', description: '要追加/前插的文本(默认按 Markdown 写;既有条目保持它自己的内容类型)', required: true },
|
|
1107
1134
|
mode: { type: 'string', enum: ['append', 'prepend'], description: '可选:append(默认,追加到末尾)/ prepend(插到开头)' },
|
|
1108
|
-
heading: { type: 'string', description: '可选:append
|
|
1135
|
+
heading: { type: 'string', description: '可选:append 时改为写入指定标题的位置(Markdown # 或 wikitext ! 标题,按标题文本匹配、忽略标题级别)。**落点 = 该标题之后、下一个任意级别标题之前**:若该标题后紧跟子标题,内容插在两者之间(即章节开头),**不是整节末尾**。标题不存在时不报错,改为追加到文末,并在回执与返回的 headingMatched=false 里明确说明(别把「没命中」当「写进去了」)' },
|
|
1109
1136
|
createIfMissing: { type: 'boolean', description: '可选:条目不存在时是否新建(默认 true)' },
|
|
1110
1137
|
tags: { type: 'array', items: { type: 'string' }, description: '可选:标签(不传则保留既有条目的原标签,传空数组 [] 表示清空全部标签;新建条目会额外自动补 agent-written)' },
|
|
1111
1138
|
fields: { type: 'object', additionalProperties: true, description: '可选:显式覆盖的自定义字段(如 {"type":"text/css"})。不传则保留既有条目的原字段与内容类型' },
|
|
@@ -1116,7 +1143,14 @@ export function registerTiddlywikiTools(ctx: ToolsCtx, deps: ToolsDeps): Array<(
|
|
|
1116
1143
|
normalizeArgs: normalizeFieldsInArgs,
|
|
1117
1144
|
output: {
|
|
1118
1145
|
render: (_args, value: AppendResult) => {
|
|
1119
|
-
|
|
1146
|
+
// v0.26.6:标题没定位到时必须说出来。以前回执无条件打「段落「X」」,
|
|
1147
|
+
// 于是「静默追加到文末」看起来跟「精确写进那一节」一模一样。
|
|
1148
|
+
const headingNote = value.heading === null
|
|
1149
|
+
? ''
|
|
1150
|
+
: value.headingMatched === false
|
|
1151
|
+
? ` · ⚠️ 未找到标题「${value.heading}」,已改为追加到文末`
|
|
1152
|
+
: ` · 段落「${value.heading}」`
|
|
1153
|
+
const lines = [`${value.created ? '已新建并写入' : '已增量写入'} tiddler「${value.title}」(${value.mode}${headingNote}):新增 ${value.added} 字符,现共 ${value.total} 字符。`]
|
|
1120
1154
|
if (value.type !== null) lines.push(`类型: ${value.type}${value.typeDefaulted === true ? '(新建且未指定,已默认 markdown)' : ''}`)
|
|
1121
1155
|
if (value.typeChanged !== undefined) lines.push(`⚠️ 内容类型已从 ${value.typeChanged.from} 改为 ${value.typeChanged.to}`)
|
|
1122
1156
|
if (value.workspace !== undefined) lines.push(`已自动标记工作区: ${WORKSPACE_TAG_PREFIX}${value.workspace}(字段 ${WORKSPACE_FIELD})`)
|
|
@@ -1145,8 +1179,11 @@ export function registerTiddlywikiTools(ctx: ToolsCtx, deps: ToolsDeps): Array<(
|
|
|
1145
1179
|
const base = existing?.text ?? ''
|
|
1146
1180
|
const addition = args.text
|
|
1147
1181
|
let next: string
|
|
1182
|
+
let headingMatched: boolean | undefined
|
|
1148
1183
|
if (mode === 'append' && typeof args.heading === 'string' && args.heading.trim().length > 0) {
|
|
1149
|
-
|
|
1184
|
+
const placed = insertIntoSection(base, args.heading.trim(), addition)
|
|
1185
|
+
next = placed.text
|
|
1186
|
+
headingMatched = placed.matched
|
|
1150
1187
|
} else if (mode === 'prepend') {
|
|
1151
1188
|
next = base.trim().length === 0 ? addition : `${addition}\n\n${base}`
|
|
1152
1189
|
} else {
|
|
@@ -1177,6 +1214,10 @@ export function registerTiddlywikiTools(ctx: ToolsCtx, deps: ToolsDeps): Array<(
|
|
|
1177
1214
|
heading: mode === 'append' && typeof args.heading === 'string' && args.heading.trim().length > 0
|
|
1178
1215
|
? args.heading.trim()
|
|
1179
1216
|
: null,
|
|
1217
|
+
// v0.26.6:给了 heading 时它到底有没有定位成功。false = 没找到该标题、文本被
|
|
1218
|
+
// 追加到了**文末**。此前这个回退是静默的,回执还照打「段落「X」」,
|
|
1219
|
+
// 导致一次定位失败看起来完全像成功(Agent 无法自检)。
|
|
1220
|
+
...(headingMatched !== undefined ? { headingMatched } : {}),
|
|
1180
1221
|
created: existing === undefined,
|
|
1181
1222
|
added: addition.length,
|
|
1182
1223
|
total: next.length,
|
|
@@ -1716,7 +1757,7 @@ interface RenameResult { ok: boolean; from: string; to: string; refsUpdated: num
|
|
|
1716
1757
|
interface DeleteResult { ok: boolean; title: string; /** true = moved to the trash (recoverable). */ trashed?: boolean; trashTitle?: string }
|
|
1717
1758
|
interface TrashItem { title: string; at?: string; of?: string }
|
|
1718
1759
|
interface TrashResult { action: string; message: string; items?: TrashItem[] }
|
|
1719
|
-
interface AppendResult { ok: boolean; title: string; mode: 'append' | 'prepend'; heading: string | null; created: boolean; added: number; total: number; type?: string | null; typeDefaulted?: boolean; typeChanged?: { from: string; to: string }; /** 新建时自动打上的工作区标记(v0.24.0)。 */ workspace?: string }
|
|
1760
|
+
interface AppendResult { ok: boolean; title: string; mode: 'append' | 'prepend'; heading: string | null; /** v0.26.6:给了 heading 时该标题是否真的定位成功(false = 未找到,文本已追加到文末)。 */ headingMatched?: boolean; created: boolean; added: number; total: number; type?: string | null; typeDefaulted?: boolean; typeChanged?: { from: string; to: string }; /** 新建时自动打上的工作区标记(v0.24.0)。 */ workspace?: string }
|
|
1720
1761
|
interface BacklinkHit { title: string; refs: number; via: 'link' | 'tag'; modified: string | null }
|
|
1721
1762
|
interface BacklinkResult { title: string; total: number; linkCount: number; tagCount: number; items: BacklinkHit[] }
|
|
1722
1763
|
interface AttachResult { ok: boolean; title: string; mime: string; bytes: number; chars: number; source: string | null; embedInto: string | null }
|