@alpacachen/dsh-kanban 1.3.7 → 1.4.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 CHANGED
@@ -27,7 +27,7 @@ You can take over at any time—drag a card, refine a note, reorder a list—and
27
27
 
28
28
  | 💬 Plan naturally | 🧭 Stay in control | ⚡ Start instantly |
29
29
  | --- | --- | --- |
30
- | Manage the board with 14 built-in `kanban_*` tools. | Edit, drag, prioritize, label, and filter from the UI. | Install the prebuilt plugin—no frontend setup required. |
30
+ | Manage the board with 15 built-in `kanban_*` tools. | Edit, comment, drag, prioritize, label, and filter from the UI. | Install the prebuilt plugin—no frontend setup required. |
31
31
  | **🗂️ Keep work separate** | **🌓 Feel at home** | **🤝 Share one source of truth** |
32
32
  | Every DSH workspace gets its own board. | Follows your DSH language, theme, and dark mode. | You and the agent always see the same tasks and status. |
33
33
 
@@ -67,8 +67,9 @@ The cards appear directly on your board. Continue in conversation, or switch to
67
67
 
68
68
  - Create, edit, delete, and drag cards between lists
69
69
  - Add notes, labels, and P0 / P1 / P2 priorities
70
+ - Exchange persistent card comments with the agent and see comment counts at a glance
70
71
  - Open a card's activity timeline to see when it changed, what changed, and whether you or the agent made the change
71
- - Track card creation, moves, title and note edits, label changes, and priority changes
72
+ - Track card creation, comments, moves, title and note edits, label changes, and priority changes
72
73
  - Send any card to the agent in your current conversation or a new one
73
74
  - Create, rename, reorder, and remove workflow lists
74
75
  - Create color-coded labels for fast visual scanning
@@ -85,7 +86,7 @@ The plugin automatically gives the agent these tools in every workspace:
85
86
  | Area | Tools |
86
87
  | --- | --- |
87
88
  | **Board** | `kanban_get` |
88
- | **Cards** | `kanban_get_card`, `kanban_add_card`, `kanban_update_card`, `kanban_delete_card`, `kanban_move_card` |
89
+ | **Cards** | `kanban_get_card`, `kanban_add_comment`, `kanban_add_card`, `kanban_update_card`, `kanban_delete_card`, `kanban_move_card` |
89
90
  | **Lists** | `kanban_add_column`, `kanban_rename_column`, `kanban_delete_column`, `kanban_move_column` |
90
91
  | **Labels** | `kanban_get_label`, `kanban_add_label`, `kanban_update_label`, `kanban_delete_label` |
91
92
 
package/README.zh.md CHANGED
@@ -27,7 +27,7 @@
27
27
 
28
28
  | 💬 自然对话 | 🧭 随时掌控 | ⚡ 开箱即用 |
29
29
  | --- | --- | --- |
30
- | 通过 14 个 `kanban_*` 工具直接管理任务。 | 在界面中编辑、拖拽、标记和筛选卡片。 | 浏览器端已预构建,无需配置前端环境。 |
30
+ | 通过 15 个 `kanban_*` 工具直接管理任务。 | 在界面中编辑、评论、拖拽、标记和筛选卡片。 | 浏览器端已预构建,无需配置前端环境。 |
31
31
  | **🗂️ 项目隔离** | **🌓 原生体验** | **🤝 实时协作** |
32
32
  | 每个 DSH 工作区都有自己的独立看板。 | 自动跟随 DSH 的语言、主题与暗色模式。 | 你做的调整,Agent 立即可见;反之亦然。 |
33
33
 
@@ -67,8 +67,9 @@ dsh --profile web --dump-config
67
67
 
68
68
  - 创建、编辑、删除卡片,并将卡片拖到其他列表
69
69
  - 为卡片补充备注、标签和 P0 / P1 / P2 优先级
70
+ - 与 Agent 在卡片下持续评论,并在看板上快速查看评论数量
70
71
  - 打开卡片活动时间线,查看变更时间、具体内容,以及操作来自你还是 Agent
71
- - 追踪卡片创建、移动、标题与备注编辑、标签变化和优先级变化
72
+ - 追踪卡片创建、评论、移动、标题与备注编辑、标签变化和优先级变化
72
73
  - 从任意卡片发起对话,在当前会话或新会话中让 Agent 接着处理
73
74
  - 创建、重命名、排序和删除工作流列表
74
75
  - 创建彩色标签,让不同任务一眼可辨
@@ -85,7 +86,7 @@ dsh --profile web --dump-config
85
86
  | 范围 | 工具 |
86
87
  | --- | --- |
87
88
  | **看板** | `kanban_get` |
88
- | **卡片** | `kanban_get_card`、`kanban_add_card`、`kanban_update_card`、`kanban_delete_card`、`kanban_move_card` |
89
+ | **卡片** | `kanban_get_card`、`kanban_add_comment`、`kanban_add_card`、`kanban_update_card`、`kanban_delete_card`、`kanban_move_card` |
89
90
  | **列表** | `kanban_add_column`、`kanban_rename_column`、`kanban_delete_column`、`kanban_move_column` |
90
91
  | **标签** | `kanban_get_label`、`kanban_add_label`、`kanban_update_label`、`kanban_delete_label` |
91
92
 
package/index.js CHANGED
@@ -7,14 +7,15 @@
7
7
  * 职责:
8
8
  * - 按工作区(项目)隔离:boards 以 workspaceId 为键,每个工作区一块独立看板
9
9
  * - 磁盘持久化:经 ctx.fs 写入 <workspace.path>/.dsh-kanban.json
10
- * - 模型工具:经 ctx.tools.register 注册 14 个 kanban_* 工具
10
+ * - 模型工具:经 ctx.tools.register 注册 15 个 kanban_* 工具
11
11
  * - 浏览器数据层:经 ctx.get('webServer') 注册 /api/kanban 前缀路由
12
12
  *
13
13
  * 数据模型(每工作区,磁盘文件带 schemaVersion):
14
- * schemaVersion: 2
14
+ * schemaVersion: 3
15
15
  * columns: [{ id, title }]
16
16
  * labels: [{ name, color }] —— 标签与颜色绑定,name 为唯一键
17
- * cards: [{ id, columnId, title, note, label, priority, createdAt, createdBy }]
17
+ * cards: [{ id, columnId, title, note, label, priority, createdAt, createdBy,
18
+ * comments: [{ id, content, source, createdAt }] }]
18
19
  * activities: [{ id, ts, cardId, type, source, field?, from?, to?, meta? }] —— 追加式活动日志
19
20
  *
20
21
  * 数据安全:
@@ -44,7 +45,7 @@ export const inject = ['tools']
44
45
  // 旧文件在首次打开时自动沿迁移链升级,升级前原文件先备份。
45
46
  // ---------------------------------------------------------------------------
46
47
 
47
- export const SCHEMA_VERSION = 2
48
+ export const SCHEMA_VERSION = 3
48
49
  export const LEGACY_VERSION = 0
49
50
 
50
51
  // 活动日志:追加式只读记录,随看板一起落盘;超过上限丢弃最旧事件,防止日志无界增长。
@@ -104,6 +105,20 @@ export const MIGRATIONS = {
104
105
  activities: [],
105
106
  }
106
107
  },
108
+ 2: (data) => {
109
+ // v2 → v3:卡片新增评论数组;历史卡片默认没有评论
110
+ const src = isObj(data) ? data : {}
111
+ const pick = (arr) => (Array.isArray(arr) ? arr : [])
112
+ return {
113
+ schemaVersion: 3,
114
+ columns: pick(src.columns),
115
+ labels: pick(src.labels),
116
+ cards: pick(src.cards)
117
+ .map((c) => (isObj(c) ? { ...c, comments: [] } : null))
118
+ .filter(Boolean),
119
+ activities: pick(src.activities),
120
+ }
121
+ },
107
122
  }
108
123
 
109
124
  /**
@@ -177,8 +192,9 @@ export function validateBoard(data) {
177
192
 
178
193
  if (Array.isArray(data.cards)) {
179
194
  const seen = new Set()
195
+ const commentIds = new Set()
180
196
  for (const c of data.cards) {
181
- if (!isObj(c) || typeof c.id !== 'string' || !c.id || typeof c.columnId !== 'string' || typeof c.title !== 'string') {
197
+ if (!isObj(c) || typeof c.id !== 'string' || !c.id || typeof c.columnId !== 'string' || typeof c.title !== 'string' || !Array.isArray(c.comments)) {
182
198
  errors.push('cards contain an invalid entry')
183
199
  break
184
200
  }
@@ -189,6 +205,17 @@ export function validateBoard(data) {
189
205
  if (!columnIds.has(c.columnId)) errors.push('card references missing column: ' + c.id)
190
206
  if (c.label != null && !labelNames.has(c.label)) errors.push('card references missing label: ' + c.id)
191
207
  if (c.priority != null && !['high', 'medium', 'low'].includes(c.priority)) errors.push('card has invalid priority: ' + c.id)
208
+ for (const comment of c.comments) {
209
+ if (!isObj(comment) || typeof comment.id !== 'string' || !comment.id || typeof comment.content !== 'string' || !comment.content.trim() || comment.content.length > 2000 || !['human', 'agent'].includes(comment.source) || typeof comment.createdAt !== 'string' || !comment.createdAt) {
210
+ errors.push('card comments contain an invalid entry: ' + c.id)
211
+ break
212
+ }
213
+ if (commentIds.has(comment.id)) {
214
+ errors.push('duplicate comment id: ' + comment.id)
215
+ break
216
+ }
217
+ commentIds.add(comment.id)
218
+ }
192
219
  seen.add(c.id)
193
220
  }
194
221
  }
@@ -444,7 +471,10 @@ export function apply(ctx) {
444
471
  }
445
472
  }
446
473
  for (const col of board.columns) bumpSeq(col.id)
447
- for (const card of board.cards) bumpSeq(card.id)
474
+ for (const card of board.cards) {
475
+ bumpSeq(card.id)
476
+ for (const comment of card.comments) bumpSeq(comment.id)
477
+ }
448
478
  for (const act of board.activities) bumpSeq(act.id)
449
479
  if (board.columns.length === 0) {
450
480
  for (const title of DEFAULT_COLUMNS) board.columns.push({ id: nextId('c'), title })
@@ -515,6 +545,14 @@ export function apply(ctx) {
515
545
  priority: c.priority ?? null,
516
546
  createdAt: typeof c.createdAt === 'string' ? c.createdAt : null,
517
547
  createdBy: typeof c.createdBy === 'string' ? c.createdBy : null,
548
+ comments: Array.isArray(c.comments)
549
+ ? c.comments.map((comment) => ({
550
+ id: comment.id,
551
+ content: comment.content,
552
+ source: comment.source,
553
+ createdAt: comment.createdAt,
554
+ }))
555
+ : [],
518
556
  })),
519
557
  activities: Array.isArray(b.activities) ? b.activities.map((a) => ({ ...a })) : [],
520
558
  })
@@ -531,6 +569,7 @@ export function apply(ctx) {
531
569
  title: c.title,
532
570
  label: c.label ?? null,
533
571
  priority: c.priority ?? null,
572
+ commentCount: c.comments.length,
534
573
  })),
535
574
  })
536
575
 
@@ -583,6 +622,7 @@ export function apply(ctx) {
583
622
  priority: normPriority(a.priority),
584
623
  createdAt: new Date().toISOString(),
585
624
  createdBy: actor,
625
+ comments: [],
586
626
  }
587
627
  board.cards.push(card)
588
628
  record(board, {
@@ -640,6 +680,24 @@ export function apply(ctx) {
640
680
  : result({ error: 'Card not found: ' + str(a.id, '') })
641
681
  }
642
682
 
683
+ case 'addComment': {
684
+ const card = findCard(board, str(a.id, ''))
685
+ if (!card) return result({ error: 'Card not found: ' + str(a.id, '') })
686
+ const content = str(a.content, '').trim()
687
+ if (!content) return result({ error: 'Comment content required' })
688
+ if (content.length > 2000) return result({ error: 'Comment exceeds 2000 characters' })
689
+ const comment = {
690
+ id: nextId('m'),
691
+ content,
692
+ source: actor,
693
+ createdAt: new Date().toISOString(),
694
+ }
695
+ card.comments.push(comment)
696
+ record(board, { cardId: card.id, type: 'card_comment_added', source: actor, meta: { title: card.title } })
697
+ await save(workspace, session)
698
+ return result({ message: 'Comment added' })
699
+ }
700
+
643
701
  case 'deleteCard': {
644
702
  const id = str(a.id, '')
645
703
  const card = findCard(board, id)
@@ -1033,7 +1091,8 @@ export function apply(ctx) {
1033
1091
  ' - [' + card.id + '] ' +
1034
1092
  (card.priority ? '[' + card.priority + '] ' : '') +
1035
1093
  (card.label ? '[' + card.label + '] ' : '') +
1036
- card.title,
1094
+ card.title +
1095
+ (card.commentCount ? ' (' + card.commentCount + ' comments)' : ''),
1037
1096
  )
1038
1097
  }
1039
1098
  }
@@ -1054,7 +1113,7 @@ export function apply(ctx) {
1054
1113
  },
1055
1114
  {
1056
1115
  name: 'kanban_get_card',
1057
- description: "Read one card's full details (title, note, label, priority) by id from the current project (workspace) board. Use kanban_get first to discover card ids, then this tool to read a card's complete note and fields.",
1116
+ description: "Read one card's full details (title, note, label, priority and comments) by id from the current project (workspace) board. Use kanban_get first to discover card ids, then this tool to read the complete card and comment history.",
1058
1117
  parameters: {
1059
1118
  type: 'object',
1060
1119
  properties: {
@@ -1086,6 +1145,12 @@ export function apply(ctx) {
1086
1145
  if (c.priority) lines.push('Priority: ' + c.priority)
1087
1146
  if (c.label) lines.push('Label: ' + c.label)
1088
1147
  if (c.note) lines.push('Note: ' + c.note)
1148
+ if (Array.isArray(c.comments) && c.comments.length > 0) {
1149
+ lines.push('Comments:')
1150
+ for (const comment of c.comments) {
1151
+ lines.push(' - [' + comment.createdAt + '] ' + comment.source + ': ' + comment.content)
1152
+ }
1153
+ }
1089
1154
  }
1090
1155
  return [{ type: 'text', text: lines.join('\n') }]
1091
1156
  },
@@ -1099,6 +1164,22 @@ export function apply(ctx) {
1099
1164
  return { ok: true, message: 'Card ' + String(args.id || '') + ' details', card: r.card, warnings: Array.isArray(r.warnings) ? r.warnings : [] }
1100
1165
  },
1101
1166
  },
1167
+ {
1168
+ name: 'kanban_add_comment',
1169
+ description: 'Add a comment to a card in the current project (workspace). Use it to leave progress updates, decisions, questions, or review feedback.',
1170
+ parameters: {
1171
+ type: 'object',
1172
+ properties: {
1173
+ id: { type: 'string', description: 'Card id' },
1174
+ content: { type: 'string', description: 'Comment text (1-2000 characters)' },
1175
+ },
1176
+ required: ['id', 'content'],
1177
+ },
1178
+ output: output(renderBoard),
1179
+ async execute(args, exec) {
1180
+ return runTool('addComment', args, exec)
1181
+ },
1182
+ },
1102
1183
  {
1103
1184
  name: 'kanban_add_card',
1104
1185
  description: 'Add a card to the current project (workspace) kanban board. Write feature breakdowns and plans to the board: one card per task step.',