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.
@@ -16,6 +16,12 @@ import type { TiddlyWebClient } from './tw-api.ts'
16
16
  /** Config tiddler (JSON string) where the settings page stores overrides. */
17
17
  export const CONFIG_TIDDLER = '$:/plugins/dsh-tiddlywiki/config'
18
18
 
19
+ /**
20
+ * Default dark palette the embedded TW switches to when DSH is dark
21
+ * (mirrored in src/client/theme-sync.ts — the two bundles cannot share code).
22
+ */
23
+ export const DARK_PALETTE_DEFAULT = '$:/palettes/CupertinoDark'
24
+
19
25
  /** Extensible, loose plugin config shape (future fields just appear here). */
20
26
  export interface PluginConfigShape {
21
27
  note?: { tag?: string }
@@ -27,6 +33,14 @@ export interface PluginConfigShape {
27
33
  showPanelStatus?: boolean
28
34
  /** 是否在界面右下角显示「同步」悬浮按钮(默认 true)。 */
29
35
  showSyncButton?: boolean
36
+ /**
37
+ * 嵌入式 TW 是否跟随 DSH 深浅主题(默认 true):DSH 暗色时把 TW 活动
38
+ * palette 临时切到 `darkPalette`,浅色时恢复用户原 palette;仅内存生效,
39
+ * 不会写回 wiki。
40
+ */
41
+ followDshTheme?: boolean
42
+ /** DSH 暗色时 TW 使用的 palette tiddler 标题(默认 CupertinoDark)。 */
43
+ darkPalette?: string
30
44
  /**
31
45
  * 一键发送给 Agent(TW 笔记 → DSH 会话注入)。
32
46
  * `enabled: false` 时路由 403、TW 端按钮点击提示未启用;`token` 非空时
@@ -73,11 +73,25 @@ export interface SessionControllerFace {
73
73
  create(request: { cwd?: string; workspaceId?: string }): Promise<{ sessionId: string }>
74
74
  }
75
75
 
76
- /** Effective UI visibility flags returned by /status (mirror index.ts). */
76
+ /**
77
+ * Structural face over the DSH `workspaceRegistry` service (a subset of
78
+ * dsh-workspace). Only what the agent-create route needs is declared; the
79
+ * runtime instance is a real Service, never inspected data.
80
+ */
81
+ export interface WorkspaceRegistryFace {
82
+ /** Resolve or create the workspace owning `path` — idempotent by canonical path. */
83
+ create(path: string, title?: string): Promise<{ id: string; path: string }>
84
+ }
85
+
86
+ /** Effective UI flags returned by /status (mirror index.ts). */
77
87
  export interface UiDefaultsPublic {
78
88
  showQuickNote: boolean
79
89
  showPanelStatus: boolean
80
90
  showSyncButton: boolean
91
+ /** 嵌入式 TW 是否跟随 DSH 深浅主题(false 时客户端停止 palette 同步)。 */
92
+ followDshTheme: boolean
93
+ /** DSH 暗色时 TW 使用的 palette tiddler 标题。 */
94
+ darkPalette: string
81
95
  }
82
96
 
83
97
  export interface RouteDeps {
@@ -91,6 +105,10 @@ export interface RouteDeps {
91
105
  /** Optional DSH sessionController service (agent-send routes only); resolved
92
106
  * lazily per request because it may register after webServer appears. */
93
107
  getSessionController: () => SessionControllerFace | undefined
108
+ /** Optional DSH workspaceRegistry service (agent-create route); resolves a
109
+ * cwd to a real Workspace so new sessions land inside it instead of the
110
+ * ungrouped bucket. */
111
+ getWorkspaceRegistry: () => WorkspaceRegistryFace | undefined
94
112
  /** Whether the one-click send-to-agent feature is enabled (config switch). */
95
113
  sendToAgentEnabled: () => boolean
96
114
  /** Optional shared token that must match `x-send-to-agent-token` when set. */
@@ -334,10 +352,14 @@ export function registerRoutes(ctx: { webServer: WebServerFace }, deps: RouteDep
334
352
  }
335
353
 
336
354
  /**
337
- * POST /dsh-tiddlywiki/agent/create — create (or adopt) one ordinary session,
338
- * optionally inside a workspace path. The picker uses it for "new workspace /
339
- * new session"; the created session's cwd becomes its workspace. The directory
340
- * is materialised so a brand-new workspace actually exists on disk.
355
+ * POST /dsh-tiddlywiki/agent/create — create (or adopt) one ordinary session
356
+ * inside a real DSH workspace resolved from the requested path. The picker
357
+ * uses it for "new workspace / new session": the directory is materialised so
358
+ * a brand-new workspace actually exists on disk, the path is resolved to its
359
+ * (idempotent) Workspace, and the session is created with `workspaceId` so it
360
+ * lands under that workspace in the sidebar. Creating with bare `cwd` instead
361
+ * would leave the session in the ungrouped bucket even when its working
362
+ * directory matches an existing workspace path.
341
363
  */
342
364
  const handleAgentCreate = async (req: IncomingMessage, res: ServerResponse): Promise<void> => {
343
365
  try {
@@ -361,10 +383,18 @@ export function registerRoutes(ctx: { webServer: WebServerFace }, deps: RouteDep
361
383
  json(res, { ok: false, error: 'session service unavailable' }, 503)
362
384
  return
363
385
  }
386
+ const ws = deps.getWorkspaceRegistry()
364
387
  if (cwd.length > 0) {
365
388
  await mkdir(cwd, { recursive: true })
366
389
  }
367
- const created = await sc.create({ cwd: cwd.length > 0 ? cwd : undefined })
390
+ let created: { sessionId: string }
391
+ if (cwd.length > 0 && ws !== undefined) {
392
+ const workspace = await ws.create(cwd)
393
+ created = await sc.create({ workspaceId: workspace.id })
394
+ json(res, { ok: true, sessionId: created.sessionId, cwd, workspaceId: workspace.id })
395
+ return
396
+ }
397
+ created = await sc.create({ cwd: cwd.length > 0 ? cwd : undefined })
368
398
  json(res, { ok: true, sessionId: created.sessionId, cwd: cwd || null })
369
399
  } catch (err) {
370
400
  json(res, { ok: false, error: err instanceof Error ? err.message : String(err) }, 500)
package/src/host/tools.ts CHANGED
@@ -18,6 +18,19 @@ import { defineTool } from '../sdk.ts'
18
18
  import type { TiddlyWebClient, Tiddler } from './tw-api.ts'
19
19
  import type { GitFace } from './git.ts'
20
20
 
21
+ /**
22
+ * 约定标签:标记「由 Agent 撰写」的笔记。
23
+ * 新建(title 不存在)时由 tiddlywiki_put / tiddlywiki_batch_put 自动补打;
24
+ * 首页据此把这类笔记单独列在「Agent 区块」并从主标签列表排除。
25
+ */
26
+ export const AGENT_WRITTEN_TAG = 'agent-written'
27
+
28
+ /**
29
+ * 约定标签:标记「Agent 撰写后又经人类编辑」的笔记(双标签分级第二档)。
30
+ * 人类在编辑某篇 Agent 笔记时手动补打,首页把这类笔记归入「Agent + 人工」档。
31
+ */
32
+ export const HUMAN_EDITED_TAG = 'human-edited'
33
+
21
34
  /** Structural tool-registry face (subset of the dsh tools service). */
22
35
  export interface ToolsCtx {
23
36
  tools: { register(tool: unknown): () => void }
@@ -83,6 +96,21 @@ function rewriteRefs(text: string, oldTitle: string, newTitle: string): { text:
83
96
  return { text: out, count }
84
97
  }
85
98
 
99
+ /**
100
+ * Compute the final tags for a write:
101
+ * - NEW tiddler (created by this tool) → auto-append the agent-written tag,
102
+ * unless it is a `$:/` system tiddler (config/language/theme internals).
103
+ * - Existing tiddler → keep the caller's tags as-is (an agent maintaining a
104
+ * human note must NOT silently claim authorship; an already agent-written
105
+ * note keeps its tag because the caller provides the full tag list).
106
+ */
107
+ function finalTagsForWrite(title: string, existing: Tiddler | undefined, tags: string[]): string[] {
108
+ if (existing !== undefined) return tags
109
+ if (title.startsWith('$:/')) return tags
110
+ if (tags.includes(AGENT_WRITTEN_TAG)) return tags
111
+ return [...tags, AGENT_WRITTEN_TAG]
112
+ }
113
+
86
114
  export function registerTiddlywikiTools(ctx: ToolsCtx, deps: ToolsDeps): Array<() => void> {
87
115
  const disposers: Array<() => void> = []
88
116
  const register = (tool: unknown): void => { disposers.push(ctx.tools.register(tool)) }
@@ -227,7 +255,7 @@ export function registerTiddlywikiTools(ctx: ToolsCtx, deps: ToolsDeps): Array<(
227
255
  // ── tiddlywiki_put ───────────────────────────────────────────────────────
228
256
  register(defineTool({
229
257
  name: 'tiddlywiki_put',
230
- description: '写入(新建或覆盖)一个 TiddlyWiki tiddler。同名覆盖;tags 为标签数组,fields 为附加自定义字段(json 对象,会写入 tiddler 字段)。写入后触发自动 commit。',
258
+ description: '写入(新建或覆盖)一个 TiddlyWiki tiddler。同名覆盖;tags 为标签数组,fields 为附加自定义字段(json 对象,会写入 tiddler 字段)。写入后触发自动 commit。新建(title 不存在)时自动补打 agent-written 标签标记「由 Agent 撰写」,无需手动添加。',
231
259
  parameters: {
232
260
  title: { type: 'string', description: 'tiddler 标题(精确匹配,覆盖同名)', required: true },
233
261
  text: { type: 'string', description: 'tiddler 全文(wiki 文本)', required: true },
@@ -249,19 +277,21 @@ export function registerTiddlywikiTools(ctx: ToolsCtx, deps: ToolsDeps): Array<(
249
277
  execute: async (args: { title: string; text: string; tags?: string[]; fields?: Record<string, unknown> }): Promise<PutResult> => {
250
278
  const wiki = deps.wiki()
251
279
  if (wiki === undefined) throw new Error('TiddlyWiki 服务未运行(tiddlywiki_status 可查)')
280
+ const existing = await wiki.get(args.title).catch(() => undefined)
281
+ const tags = finalTagsForWrite(args.title, existing, Array.isArray(args.tags) ? args.tags.filter((t) => typeof t === 'string' && t.trim().length > 0) : [])
252
282
  const tiddler: Tiddler = { title: args.title, text: args.text }
253
- if (Array.isArray(args.tags) && args.tags.length > 0) tiddler.tags = args.tags
283
+ if (tags.length > 0) tiddler.tags = tags
254
284
  if (args.fields !== undefined && typeof args.fields === 'object' && args.fields !== null) Object.assign(tiddler, args.fields)
255
285
  await wiki.put(tiddler)
256
286
  deps.autoCommit()
257
- return { ok: true, title: args.title, tags: args.tags ?? [], fields: args.fields ?? null }
287
+ return { ok: true, title: args.title, tags, fields: args.fields ?? null }
258
288
  },
259
289
  }))
260
290
 
261
291
  // ── tiddlywiki_batch_put ─────────────────────────────────────────────────
262
292
  register(defineTool({
263
293
  name: 'tiddlywiki_batch_put',
264
- description: '批量写入/覆盖多个 TiddlyWiki tiddler(一次工具调用)。overwrite=false 时跳过已存在的标题;返回逐条结果。写入后触发自动 commit。',
294
+ description: '批量写入/覆盖多个 TiddlyWiki tiddler(一次工具调用)。overwrite=false 时跳过已存在的标题;返回逐条结果。写入后触发自动 commit。新建(title 不存在)的条目会自动补打 agent-written 标签,无需手动添加。',
265
295
  parameters: {
266
296
  items: {
267
297
  type: 'array',
@@ -301,16 +331,15 @@ export function registerTiddlywikiTools(ctx: ToolsCtx, deps: ToolsDeps): Array<(
301
331
  for (const item of list) {
302
332
  if (typeof item.title !== 'string' || item.title.length === 0) throw new Error('batch_put: 每条 items 都需要非空 title')
303
333
  if (typeof item.text !== 'string') throw new Error(`batch_put: items「${item.title}」缺少 text`)
304
- if (!overwrite) {
305
- const existing = await wiki.get(item.title).catch(() => undefined)
306
- if (existing !== undefined) {
307
- skipped++
308
- results.push({ title: item.title, written: false, skipped: true })
309
- continue
310
- }
334
+ const existing = await wiki.get(item.title).catch(() => undefined)
335
+ if (!overwrite && existing !== undefined) {
336
+ skipped++
337
+ results.push({ title: item.title, written: false, skipped: true })
338
+ continue
311
339
  }
340
+ const tags = finalTagsForWrite(item.title, existing, Array.isArray(item.tags) ? item.tags.filter((t) => typeof t === 'string' && t.trim().length > 0) : [])
312
341
  const tiddler: Tiddler = { title: item.title, text: item.text }
313
- if (Array.isArray(item.tags) && item.tags.length > 0) tiddler.tags = item.tags
342
+ if (tags.length > 0) tiddler.tags = tags
314
343
  if (item.fields !== undefined && typeof item.fields === 'object' && item.fields !== null) Object.assign(tiddler, item.fields)
315
344
  await wiki.put(tiddler)
316
345
  written++
package/src/index.ts CHANGED
@@ -22,8 +22,8 @@ import { watch, type FSWatcher } from 'node:fs'
22
22
  import { writeFile } from 'node:fs/promises'
23
23
  import { join } from 'node:path'
24
24
  import { AutoCommitter, GitFace } from './host/git.ts'
25
- import { registerRoutes, type SessionControllerFace, type WebServerFace } from './host/routes.ts'
26
- import { ConfigStore, deepMerge, type PluginConfigShape } from './host/config.ts'
25
+ import { registerRoutes, type SessionControllerFace, type WebServerFace, type WorkspaceRegistryFace } from './host/routes.ts'
26
+ import { ConfigStore, deepMerge, DARK_PALETTE_DEFAULT, type PluginConfigShape } from './host/config.ts'
27
27
  import { registerAdminRoutes, ensureLanguage, resolveTwRoot, type AdminDeps } from './host/admin.ts'
28
28
  import { seedDocNote, DOC_NOTE_TITLE } from './host/seed-notes.ts'
29
29
  import { TiddlyWebClient } from './host/tw-api.ts'
@@ -56,7 +56,7 @@ export interface TiddlywikiConfig {
56
56
  port?: number
57
57
  git?: { autoCommit?: boolean; debounceMs?: number; remote?: string; branch?: string }
58
58
  note?: { tag?: string }
59
- ui?: { showQuickNote?: boolean; showPanelStatus?: boolean; showSyncButton?: boolean }
59
+ ui?: { showQuickNote?: boolean; showPanelStatus?: boolean; showSyncButton?: boolean; followDshTheme?: boolean; darkPalette?: string }
60
60
  auth?: { username?: string; password?: string }
61
61
  }
62
62
 
@@ -77,7 +77,7 @@ interface ResolvedConfig {
77
77
  port: number
78
78
  git: { autoCommit: boolean; debounceMs: number; remote: string; branch: string }
79
79
  note: { tag: string }
80
- ui: { showQuickNote: boolean; showPanelStatus: boolean; showSyncButton: boolean; sendToAgent: { enabled: boolean } }
80
+ ui: { showQuickNote: boolean; showPanelStatus: boolean; showSyncButton: boolean; followDshTheme: boolean; darkPalette: string; sendToAgent: { enabled: boolean } }
81
81
  auth: { username?: string; password?: string }
82
82
  }
83
83
 
@@ -87,7 +87,7 @@ const DEFAULTS: ResolvedConfig = {
87
87
  port: 0,
88
88
  git: { autoCommit: true, debounceMs: 60_000, remote: '', branch: 'main' },
89
89
  note: { tag: 'inbox' },
90
- ui: { showQuickNote: true, showPanelStatus: true, showSyncButton: true, sendToAgent: { enabled: true } },
90
+ ui: { showQuickNote: true, showPanelStatus: true, showSyncButton: true, followDshTheme: true, darkPalette: DARK_PALETTE_DEFAULT, sendToAgent: { enabled: true } },
91
91
  auth: { username: '', password: '' },
92
92
  }
93
93
 
@@ -183,7 +183,9 @@ pull 冲突后:先 \`tiddlywiki_git_resolve files=[冲突文件] strategy=keep
183
183
 
184
184
  把 wiki 当作长期记忆与知识沉淀的地方:会议纪要、决策记录、调研笔记、随手的想法都可存成独立 tiddler(tag 建议用 inbox/meeting/decision 等便于检索)。
185
185
 
186
- 用本插件自动创建笔记时,除了业务性 tag 外,请把「当前工作区(项目)的名字」也作为标签之一加上去(例如 \`tiddlywiki_put\` 的 tags 里带上当前 workspace 名),这样笔记能按项目归集、检索。`
186
+ 用本插件自动创建笔记时,除了业务性 tag 外,请把「当前工作区(项目)的名字」也作为标签之一加上去(例如 \`tiddlywiki_put\` 的 tags 里带上当前 workspace 名),这样笔记能按项目归集、检索。
187
+
188
+ **Agent 笔记标签约定**:\`tiddlywiki_put\` / \`tiddlywiki_batch_put\` 新建笔记时,插件会自动补打 \`agent-written\` 标签(标记「由 Agent 撰写」),无需手动添加,也不要手动移除它(除非用户明确要求)。首页会把 Agent 笔记单独列在「Agent 区块」,主标签列表只统计人类笔记。若某篇 Agent 笔记后续被人类编辑过,请在该笔记上补打 \`human-edited\` 标签,首页会把它归入「Agent + 人工」档。覆盖写入已有的(人类)笔记时不会自动加 agent-written,请保持笔记原本的归属。`
187
189
 
188
190
  /**
189
191
  * Mount the host half.
@@ -212,11 +214,17 @@ export function apply(ctx: HostCtx, rawConfig: TiddlywikiConfig = {}): void {
212
214
  const tag = eff().note?.tag
213
215
  return typeof tag === 'string' && tag.trim().length > 0 ? tag : config.note.tag
214
216
  }
215
- const effectiveUi = (): { showQuickNote: boolean; showPanelStatus: boolean; showSyncButton: boolean } => ({
216
- showQuickNote: eff().ui?.showQuickNote !== false,
217
- showPanelStatus: eff().ui?.showPanelStatus !== false,
218
- showSyncButton: eff().ui?.showSyncButton !== false,
219
- })
217
+ const effectiveUi = (): { showQuickNote: boolean; showPanelStatus: boolean; showSyncButton: boolean; followDshTheme: boolean; darkPalette: string } => {
218
+ const ui = eff().ui ?? {}
219
+ const palette = typeof ui.darkPalette === 'string' && ui.darkPalette.trim().length > 0 ? ui.darkPalette.trim() : DARK_PALETTE_DEFAULT
220
+ return {
221
+ showQuickNote: ui.showQuickNote !== false,
222
+ showPanelStatus: ui.showPanelStatus !== false,
223
+ showSyncButton: ui.showSyncButton !== false,
224
+ followDshTheme: ui.followDshTheme !== false,
225
+ darkPalette: palette,
226
+ }
227
+ }
220
228
 
221
229
  const disposers: Array<() => void> = []
222
230
  const disposeAll = (): void => {
@@ -359,6 +367,8 @@ export function apply(ctx: HostCtx, rawConfig: TiddlywikiConfig = {}): void {
359
367
  // agent routes must work whenever a request actually arrives.
360
368
  const getSessionController = (): SessionControllerFace | undefined =>
361
369
  ctx.get('sessionController') as SessionControllerFace | undefined
370
+ const getWorkspaceRegistry = (): WorkspaceRegistryFace | undefined =>
371
+ ctx.get('workspaceRegistry') as WorkspaceRegistryFace | undefined
362
372
  const disposeRoutes = registerRoutes({ webServer: ws }, {
363
373
  server,
364
374
  getClient: client,
@@ -368,6 +378,7 @@ export function apply(ctx: HostCtx, rawConfig: TiddlywikiConfig = {}): void {
368
378
  uiDefaults: () => effectiveUi(),
369
379
  getWikiPath: () => wikiPath,
370
380
  getSessionController,
381
+ getWorkspaceRegistry,
371
382
  sendToAgentEnabled: () => eff().ui?.sendToAgent?.enabled !== false,
372
383
  sendToAgentToken: () => {
373
384
  const token = eff().ui?.sendToAgent?.token