@qnote/q-ai-note 1.0.4 → 1.0.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.
@@ -14,7 +14,7 @@ function byId(targetId) {
14
14
  export function mountSandboxGrid(targetId, options) {
15
15
  const container = byId(targetId);
16
16
  if (!container) return;
17
- const { sandboxes = [], emptyText = '暂无数据', onOpen, onDelete } = options || {};
17
+ const { sandboxes = [], emptyText = '暂无数据', onOpen, onDelete, readonly = false } = options || {};
18
18
 
19
19
  if (!sandboxes.length) {
20
20
  container.innerHTML = `<div class="empty-state"><p>${esc(emptyText)}</p></div>`;
@@ -28,7 +28,7 @@ export function mountSandboxGrid(targetId, options) {
28
28
  <div class="sandbox-meta">
29
29
  <span>${esc(new Date(sandbox.updated_at).toLocaleDateString())}</span>
30
30
  </div>
31
- <button class="btn btn-secondary btn-sm sandbox-delete" data-delete-id="${esc(sandbox.id)}">删除</button>
31
+ ${readonly ? '' : `<button class="btn btn-secondary btn-sm sandbox-delete" data-delete-id="${esc(sandbox.id)}">删除</button>`}
32
32
  </div>
33
33
  `).join('');
34
34
 
@@ -52,7 +52,17 @@ export function mountHtmlList(targetId, htmlItems) {
52
52
  export function mountDiaryTimeline(targetId, options) {
53
53
  const container = byId(targetId);
54
54
  if (!container) return;
55
- const { diaries = [], getSandboxName, onConfirm, onIgnore } = options || {};
55
+ const {
56
+ diaries = [],
57
+ getSandboxName,
58
+ getWorkItemName,
59
+ onConfirm,
60
+ onIgnore,
61
+ onEdit,
62
+ onOpenWorkItem,
63
+ readonly = false,
64
+ renderContent,
65
+ } = options || {};
56
66
 
57
67
  if (!diaries.length) {
58
68
  container.innerHTML = '<div class="empty-state"><p>暂无日记</p></div>';
@@ -61,17 +71,31 @@ export function mountDiaryTimeline(targetId, options) {
61
71
 
62
72
  container.innerHTML = diaries.map((diary) => `
63
73
  <div class="diary-item ${diary.processed ? 'processed' : ''}">
64
- <div class="diary-meta">
65
- ${esc(new Date(diary.created_at).toLocaleString())}
66
- ${diary.sandbox_id ? ` · ${esc(getSandboxName?.(diary.sandbox_id) || diary.sandbox_id)}` : ''}
67
- </div>
68
- <div class="diary-content">${esc(diary.content)}</div>
69
- ${diary.processed ? '' : `
70
- <div class="diary-actions">
71
- <button class="confirm" data-confirm-id="${esc(diary.id)}">采纳</button>
72
- <button class="ignore" data-ignore-id="${esc(diary.id)}">忽略</button>
74
+ <div class="diary-head-row">
75
+ <div class="diary-meta">
76
+ ${esc(new Date(diary.created_at).toLocaleString())}
77
+ ${diary.sandbox_id ? ` · ${esc(getSandboxName?.(diary.sandbox_id) || diary.sandbox_id)}` : ''}
78
+ ${diary.work_item_id ? `
79
+ · 节点:
80
+ <button
81
+ class="diary-open-node-link"
82
+ type="button"
83
+ data-open-work-item="${esc(diary.work_item_id)}"
84
+ data-open-sandbox="${esc(diary.sandbox_id || '')}"
85
+ >${esc(getWorkItemName?.(diary.sandbox_id, diary.work_item_id) || diary.work_item_id)}</button>
86
+ ` : ''}
73
87
  </div>
74
- `}
88
+ ${readonly ? '' : `
89
+ <div class="diary-actions">
90
+ <button class="edit" data-edit-id="${esc(diary.id)}">编辑</button>
91
+ ${diary.processed ? '' : `
92
+ <button class="confirm" data-confirm-id="${esc(diary.id)}">采纳</button>
93
+ <button class="ignore" data-ignore-id="${esc(diary.id)}">忽略</button>
94
+ `}
95
+ </div>
96
+ `}
97
+ </div>
98
+ <div class="diary-content">${typeof renderContent === 'function' ? renderContent(diary.content) : esc(diary.content)}</div>
75
99
  </div>
76
100
  `).join('');
77
101
 
@@ -81,15 +105,28 @@ export function mountDiaryTimeline(targetId, options) {
81
105
  container.querySelectorAll('[data-ignore-id]').forEach((el) => {
82
106
  el.addEventListener('click', () => onIgnore?.(el.getAttribute('data-ignore-id')));
83
107
  });
108
+ container.querySelectorAll('[data-edit-id]').forEach((el) => {
109
+ el.addEventListener('click', () => onEdit?.(el.getAttribute('data-edit-id')));
110
+ });
111
+ container.querySelectorAll('[data-open-work-item]').forEach((el) => {
112
+ el.addEventListener('click', () => {
113
+ const workItemId = el.getAttribute('data-open-work-item');
114
+ const sandboxId = el.getAttribute('data-open-sandbox');
115
+ if (!workItemId || !sandboxId) return;
116
+ onOpenWorkItem?.(sandboxId, workItemId);
117
+ });
118
+ });
84
119
  }
85
120
 
86
121
  function renderEntityBadges(summary) {
87
122
  const issue = Number(summary?.issue || 0);
88
123
  const knowledge = Number(summary?.knowledge || 0);
89
124
  const capability = Number(summary?.capability || 0);
125
+ if (issue + knowledge + capability === 0) {
126
+ return '';
127
+ }
90
128
  const issueOpen = Number(summary?.issue_open || 0);
91
129
  const issueClosed = Number(summary?.issue_closed || 0);
92
- const isBlindSpot = issue + knowledge + capability === 0;
93
130
  const issueStateClass = issue === 0
94
131
  ? 'issue-none'
95
132
  : issueOpen > 0
@@ -99,33 +136,70 @@ function renderEntityBadges(summary) {
99
136
  : 'issue-none';
100
137
  return `
101
138
  <span
102
- class="node-entity-mini-badges ${isBlindSpot ? 'blind-spot' : ''} ${issueStateClass}"
139
+ class="node-entity-mini-badges ${issueStateClass}"
103
140
  title="节点摘要:${issue}/${knowledge}/${capability}(Issue/Knowledge/Capability)"
104
141
  >
105
- <span class="node-entity-mini-badge ${issue > 0 || knowledge > 0 || capability > 0 ? 'active' : ''}">${issue}/${knowledge}/${capability}</span>
142
+ <span class="node-entity-mini-badge active">${issue}/${knowledge}/${capability}</span>
106
143
  </span>
107
144
  `;
108
145
  }
109
146
 
110
- function renderTreeNode(node, byParent, expandedIdSet, entitySummaryByNodeId, showAssignee = false) {
147
+ function pickEntityPreviewRows(nodeId, entityRowsByNodeId, mode) {
148
+ if (!entityRowsByNodeId || mode === 'none') return [];
149
+ const rows = entityRowsByNodeId[nodeId] || [];
150
+ if (mode === 'all') return rows;
151
+ return rows.filter((row) => row.entity_type === mode);
152
+ }
153
+
154
+ function renderEntityPreviewBoxes(nodeId, entityRowsByNodeId, mode) {
155
+ const rows = pickEntityPreviewRows(nodeId, entityRowsByNodeId, mode);
156
+ if (!rows.length) return '';
157
+ const shownRows = rows.slice(0, 4);
158
+ return `
159
+ <div class="node-entity-preview-strip" data-node-id="${esc(nodeId)}">
160
+ ${shownRows.map((row) => `
161
+ <button
162
+ class="entity-preview-box ${esc(row.entity_type)}"
163
+ type="button"
164
+ data-action="entity-preview"
165
+ data-node-id="${esc(nodeId)}"
166
+ data-entity-type="${esc(row.entity_type)}"
167
+ data-entity-id="${esc(row.id)}"
168
+ title="${esc(row.title || row.entity_type)}"
169
+ >
170
+ <span class="entity-preview-type">${esc(row.entity_type)}</span>
171
+ <span class="entity-preview-title">${esc(row.title || '-')}</span>
172
+ </button>
173
+ `).join('')}
174
+ ${rows.length > shownRows.length ? `<span class="entity-preview-more">+${rows.length - shownRows.length}</span>` : ''}
175
+ </div>
176
+ `;
177
+ }
178
+
179
+ function renderTreeNode(node, byParent, expandedIdSet, entitySummaryByNodeId, showAssignee = false, entityRowsByNodeId = {}, elementPreviewMode = 'none', readonly = false, selectedId = '') {
111
180
  const children = byParent.get(node.id) || [];
112
181
  const hasChildren = children.length > 0;
113
182
  const isExpanded = expandedIdSet.has(node.id);
114
183
  const isShort = String(node.name || '').trim().length <= 10;
115
184
  const nodeSummary = entitySummaryByNodeId?.[node.id] || { issue: 0, knowledge: 0, capability: 0 };
185
+ const previewHtml = renderEntityPreviewBoxes(node.id, entityRowsByNodeId, elementPreviewMode);
186
+ const isSelected = String(selectedId || '') === String(node.id || '');
116
187
 
117
188
  if (!hasChildren) {
118
189
  return `
119
- <div class="tree-leaf-node" data-id="${esc(node.id)}" data-select-id="${esc(node.id)}" tabindex="0">
120
- <span class="node-status ${esc(node.status)}"></span>
190
+ <div class="tree-leaf-node ${isSelected ? 'is-selected' : ''}" data-id="${esc(node.id)}" data-select-id="${esc(node.id)}" tabindex="0">
121
191
  <span class="node-name ${isShort ? 'short-name' : ''}" data-select-id="${esc(node.id)}">${esc(node.name)}</span>
122
192
  ${renderEntityBadges(nodeSummary)}
123
- <div class="node-actions">
124
- <button class="node-action-btn chat" data-action="quick-chat" data-id="${esc(node.id)}" title="快捷提问">💬</button>
125
- <button class="node-action-btn add-child" data-action="add-child" data-id="${esc(node.id)}" title="添加子任务">+</button>
126
- <button class="node-action-btn" data-action="edit" data-id="${esc(node.id)}" title="编辑">✎</button>
127
- <button class="node-action-btn delete" data-action="delete" data-id="${esc(node.id)}" title="删除">✕</button>
128
- </div>
193
+ ${previewHtml}
194
+ ${readonly ? '' : `
195
+ <div class="node-actions">
196
+ <button class="node-action-btn chat" data-action="quick-chat" data-id="${esc(node.id)}" title="快捷提问">💬</button>
197
+ <button class="node-action-btn" data-action="add-diary" data-id="${esc(node.id)}" title="记录日记">📝</button>
198
+ <button class="node-action-btn add-child" data-action="add-child" data-id="${esc(node.id)}" title="添加子任务">+</button>
199
+ <button class="node-action-btn" data-action="edit" data-id="${esc(node.id)}" title="编辑">✎</button>
200
+ <button class="node-action-btn delete" data-action="delete" data-id="${esc(node.id)}" title="删除">✕</button>
201
+ </div>
202
+ `}
129
203
  </div>
130
204
  `;
131
205
  }
@@ -135,7 +209,7 @@ function renderTreeNode(node, byParent, expandedIdSet, entitySummaryByNodeId, sh
135
209
 
136
210
  return `
137
211
  <div class="tree-parent-card ${isExpanded ? 'expanded' : 'collapsed'}">
138
- <div class="tree-parent-header" data-select-id="${esc(node.id)}" tabindex="0" aria-expanded="${isExpanded ? 'true' : 'false'}">
212
+ <div class="tree-parent-header ${isSelected ? 'is-selected' : ''}" data-select-id="${esc(node.id)}" tabindex="0" aria-expanded="${isExpanded ? 'true' : 'false'}">
139
213
  <button
140
214
  class="node-expand-btn ${isExpanded ? 'expanded' : ''}"
141
215
  data-action="toggle"
@@ -146,37 +220,44 @@ function renderTreeNode(node, byParent, expandedIdSet, entitySummaryByNodeId, sh
146
220
  >
147
221
  <svg viewBox="0 0 24 24" fill="currentColor"><path d="M8.59 16.59L13.17 12 8.59 7.41 10 6l6 6-6 6-1.41-1.41z"/></svg>
148
222
  </button>
149
- <span class="node-status ${esc(node.status)}"></span>
150
223
  <span class="node-name">${esc(node.name)}</span>
151
224
  ${renderEntityBadges(nodeSummary)}
152
225
  ${showAssignee && node.assignee ? `<span class="node-meta">@${esc(node.assignee)}</span>` : ''}
153
- <div class="node-actions">
154
- <button class="node-action-btn chat" data-action="quick-chat" data-id="${esc(node.id)}" title="快捷提问">💬</button>
155
- <button class="node-action-btn add-child" data-action="add-child" data-id="${esc(node.id)}" title="添加子任务">+</button>
156
- <button class="node-action-btn" data-action="edit" data-id="${esc(node.id)}" title="编辑">✎</button>
157
- <button class="node-action-btn delete" data-action="delete" data-id="${esc(node.id)}" title="删除">✕</button>
158
- </div>
226
+ ${readonly ? '' : `
227
+ <div class="node-actions">
228
+ <button class="node-action-btn chat" data-action="quick-chat" data-id="${esc(node.id)}" title="快捷提问">💬</button>
229
+ <button class="node-action-btn" data-action="add-diary" data-id="${esc(node.id)}" title="记录日记">📝</button>
230
+ <button class="node-action-btn add-child" data-action="add-child" data-id="${esc(node.id)}" title="添加子任务">+</button>
231
+ <button class="node-action-btn" data-action="edit" data-id="${esc(node.id)}" title="编辑">✎</button>
232
+ <button class="node-action-btn delete" data-action="delete" data-id="${esc(node.id)}" title="删除">✕</button>
233
+ </div>
234
+ `}
159
235
  </div>
160
236
  ${isExpanded ? `
237
+ ${previewHtml ? `<div class="tree-node-preview-wrapper">${previewHtml}</div>` : ''}
161
238
  <div class="tree-parent-children">
162
- ${branchChildren.map((child) => renderTreeNode(child, byParent, expandedIdSet, entitySummaryByNodeId, showAssignee)).join('')}
239
+ ${branchChildren.map((child) => renderTreeNode(child, byParent, expandedIdSet, entitySummaryByNodeId, showAssignee, entityRowsByNodeId, elementPreviewMode, readonly, selectedId)).join('')}
163
240
  ${leafChildren.length ? `
164
241
  <div class="tree-leaf-container">
165
242
  <div class="tree-leaf-grid">
166
243
  ${leafChildren.map((child) => {
167
244
  const shortName = String(child.name || '').trim().length <= 10;
168
245
  const childSummary = entitySummaryByNodeId?.[child.id] || { issue: 0, knowledge: 0, capability: 0 };
246
+ const childPreviewHtml = renderEntityPreviewBoxes(child.id, entityRowsByNodeId, elementPreviewMode);
169
247
  return `
170
- <div class="tree-leaf-node" data-id="${esc(child.id)}" data-select-id="${esc(child.id)}" tabindex="0">
171
- <span class="node-status ${esc(child.status)}"></span>
248
+ <div class="tree-leaf-node ${String(selectedId || '') === String(child.id || '') ? 'is-selected' : ''}" data-id="${esc(child.id)}" data-select-id="${esc(child.id)}" tabindex="0">
172
249
  <span class="node-name ${shortName ? 'short-name' : ''}" data-select-id="${esc(child.id)}">${esc(child.name)}</span>
173
250
  ${renderEntityBadges(childSummary)}
174
- <div class="node-actions">
175
- <button class="node-action-btn chat" data-action="quick-chat" data-id="${esc(child.id)}" title="快捷提问">💬</button>
176
- <button class="node-action-btn add-child" data-action="add-child" data-id="${esc(child.id)}" title="添加子任务">+</button>
177
- <button class="node-action-btn" data-action="edit" data-id="${esc(child.id)}" title="编辑">✎</button>
178
- <button class="node-action-btn delete" data-action="delete" data-id="${esc(child.id)}" title="删除">✕</button>
179
- </div>
251
+ ${childPreviewHtml}
252
+ ${readonly ? '' : `
253
+ <div class="node-actions">
254
+ <button class="node-action-btn chat" data-action="quick-chat" data-id="${esc(child.id)}" title="快捷提问">💬</button>
255
+ <button class="node-action-btn" data-action="add-diary" data-id="${esc(child.id)}" title="记录日记">📝</button>
256
+ <button class="node-action-btn add-child" data-action="add-child" data-id="${esc(child.id)}" title="添加子任务">+</button>
257
+ <button class="node-action-btn" data-action="edit" data-id="${esc(child.id)}" title="编辑">✎</button>
258
+ <button class="node-action-btn delete" data-action="delete" data-id="${esc(child.id)}" title="删除">✕</button>
259
+ </div>
260
+ `}
180
261
  </div>
181
262
  `;
182
263
  }).join('')}
@@ -232,18 +313,20 @@ function applyAdaptiveRootMixedGridLayout(container) {
232
313
  });
233
314
  }
234
315
 
235
- function renderDenseLaneNode(node, byParent, expandedIdSet, entitySummaryByNodeId, depth = 0, showAssignee = false) {
316
+ function renderDenseLaneNode(node, byParent, expandedIdSet, entitySummaryByNodeId, depth = 0, showAssignee = false, entityRowsByNodeId = {}, elementPreviewMode = 'none', readonly = false, selectedId = '') {
236
317
  const children = byParent.get(node.id) || [];
237
318
  const hasChildren = children.length > 0;
238
319
  const isExpanded = expandedIdSet.has(node.id);
239
320
  const nodeSummary = entitySummaryByNodeId?.[node.id] || { issue: 0, knowledge: 0, capability: 0 };
240
321
  const useStackSummary = depth >= 3;
322
+ const previewHtml = renderEntityPreviewBoxes(node.id, entityRowsByNodeId, elementPreviewMode);
241
323
  const childrenHtml = hasChildren && isExpanded
242
- ? `<div class="lane-tree-children">${children.map((child) => renderDenseLaneNode(child, byParent, expandedIdSet, entitySummaryByNodeId, depth + 1, showAssignee)).join('')}</div>`
324
+ ? `<div class="lane-tree-children">${children.map((child) => renderDenseLaneNode(child, byParent, expandedIdSet, entitySummaryByNodeId, depth + 1, showAssignee, entityRowsByNodeId, elementPreviewMode, readonly, selectedId)).join('')}</div>`
243
325
  : '';
326
+ const isSelected = String(selectedId || '') === String(node.id || '');
244
327
  return `
245
328
  <div class="lane-tree-node" data-depth="${depth}">
246
- <div class="lane-tree-node-row" data-select-id="${esc(node.id)}" data-node-id="${esc(node.id)}" tabindex="0">
329
+ <div class="lane-tree-node-row ${isSelected ? 'is-selected' : ''}" data-select-id="${esc(node.id)}" data-node-id="${esc(node.id)}" tabindex="0" draggable="${readonly ? 'false' : 'true'}">
247
330
  ${hasChildren ? `
248
331
  <button
249
332
  class="node-expand-btn dense-expand-btn ${isExpanded ? 'expanded' : ''}"
@@ -270,13 +353,16 @@ function renderDenseLaneNode(node, byParent, expandedIdSet, entitySummaryByNodeI
270
353
  </div>
271
354
  ` : ''}
272
355
  </div>
273
- <div class="node-actions">
274
- <button class="node-action-btn chat" data-action="quick-chat" data-id="${esc(node.id)}" title="快捷提问">💬</button>
275
- <button class="node-action-btn add-child" data-action="add-child" data-id="${esc(node.id)}" title="添加子任务">+</button>
276
- <button class="node-action-btn" data-action="edit" data-id="${esc(node.id)}" title="编辑">✎</button>
277
- <button class="node-action-btn delete" data-action="delete" data-id="${esc(node.id)}" title="删除">✕</button>
278
- </div>
356
+ ${readonly ? '' : `
357
+ <div class="node-actions">
358
+ <button class="node-action-btn chat" data-action="quick-chat" data-id="${esc(node.id)}" title="快捷提问">💬</button>
359
+ <button class="node-action-btn add-child" data-action="add-child" data-id="${esc(node.id)}" title="添加子任务">+</button>
360
+ <button class="node-action-btn" data-action="edit" data-id="${esc(node.id)}" title="编辑">✎</button>
361
+ <button class="node-action-btn delete" data-action="delete" data-id="${esc(node.id)}" title="删除">✕</button>
362
+ </div>
363
+ `}
279
364
  </div>
365
+ ${previewHtml ? `<div class="lane-node-preview-wrapper">${previewHtml}</div>` : ''}
280
366
  ${childrenHtml}
281
367
  </div>
282
368
  `;
@@ -316,16 +402,17 @@ function getDenseLaneWidthPx(root, byParent, showAssignee = false) {
316
402
  return Math.max(MIN_WIDTH, Math.min(MAX_WIDTH, preferred));
317
403
  }
318
404
 
319
- function renderDenseTree(roots, byParent, expandedIdSet, entitySummaryByNodeId, showAssignee = false) {
405
+ function renderDenseTree(roots, byParent, expandedIdSet, entitySummaryByNodeId, showAssignee = false, entityRowsByNodeId = {}, elementPreviewMode = 'none', readonly = false, selectedId = '') {
320
406
  return `
321
407
  <div class="dense-tree dense-horizontal dense-lane-board">
322
408
  ${roots.map((root) => {
323
409
  const laneWidth = getDenseLaneWidthPx(root, byParent, showAssignee);
324
410
  const laneNameMax = Math.max(120, laneWidth - 86);
325
411
  return `
326
- <section class="dense-lane" style="--lane-width:${laneWidth}px;--lane-name-max:${laneNameMax}px;">
412
+ <section class="dense-lane" data-root-id="${esc(root.id)}" style="--lane-width:${laneWidth}px;--lane-name-max:${laneNameMax}px;">
413
+ ${readonly ? '' : `<div class="dense-lane-drag-handle" data-lane-drag-id="${esc(root.id)}" draggable="true" title="拖拽调整泳道顺序">⋮⋮</div>`}
327
414
  <div class="dense-lane-body">
328
- ${renderDenseLaneNode(root, byParent, expandedIdSet, entitySummaryByNodeId, 0, showAssignee)}
415
+ ${renderDenseLaneNode(root, byParent, expandedIdSet, entitySummaryByNodeId, 0, showAssignee, entityRowsByNodeId, elementPreviewMode, readonly, selectedId)}
329
416
  </div>
330
417
  </section>
331
418
  `;
@@ -342,13 +429,22 @@ export function mountWorkTree(targetId, options) {
342
429
  expandedIds = [],
343
430
  onToggleExpand,
344
431
  onAddChild,
432
+ onAddDiary,
345
433
  onEdit,
346
434
  onDelete,
347
435
  onQuickChat,
348
436
  onSelect,
437
+ onSelectEntity,
438
+ onMoveNode,
439
+ onReorderSiblings,
440
+ onReorderLanes,
349
441
  entitySummaryByNodeId = {},
442
+ entityRowsByNodeId = {},
443
+ elementPreviewMode = 'none',
350
444
  renderMode = 'card',
351
445
  showAssignee = false,
446
+ readonly = false,
447
+ selectedId = '',
352
448
  } = options || {};
353
449
 
354
450
  const byParent = new Map();
@@ -357,6 +453,33 @@ export function mountWorkTree(targetId, options) {
357
453
  if (!byParent.has(key)) byParent.set(key, []);
358
454
  byParent.get(key).push(item);
359
455
  }
456
+ const laneOrderOf = (item) => {
457
+ const value = Number(item?.extra_data?.lane_order);
458
+ return Number.isFinite(value) ? value : Number.POSITIVE_INFINITY;
459
+ };
460
+ const keyOf = (item, keyName) => String(item?.extra_data?.[keyName] || '').trim();
461
+ byParent.forEach((rows, parentKey) => {
462
+ rows.sort((a, b) => {
463
+ if (parentKey === '__root__') {
464
+ const laneKeyA = keyOf(a, 'lane_order_key');
465
+ const laneKeyB = keyOf(b, 'lane_order_key');
466
+ if (laneKeyA && laneKeyB && laneKeyA !== laneKeyB) return laneKeyA.localeCompare(laneKeyB);
467
+ if (laneKeyA && !laneKeyB) return -1;
468
+ if (!laneKeyA && laneKeyB) return 1;
469
+ const laneDiff = laneOrderOf(a) - laneOrderOf(b);
470
+ if (laneDiff !== 0) return laneDiff;
471
+ } else {
472
+ const orderKeyA = keyOf(a, 'order_key');
473
+ const orderKeyB = keyOf(b, 'order_key');
474
+ if (orderKeyA && orderKeyB && orderKeyA !== orderKeyB) return orderKeyA.localeCompare(orderKeyB);
475
+ if (orderKeyA && !orderKeyB) return -1;
476
+ if (!orderKeyA && orderKeyB) return 1;
477
+ }
478
+ const timeA = String(a.created_at || '');
479
+ const timeB = String(b.created_at || '');
480
+ return timeA.localeCompare(timeB);
481
+ });
482
+ });
360
483
  const roots = byParent.get('__root__') || [];
361
484
  const expandedIdSet = new Set(expandedIds);
362
485
 
@@ -365,13 +488,13 @@ export function mountWorkTree(targetId, options) {
365
488
  return;
366
489
  }
367
490
  if (renderMode === 'dense') {
368
- container.innerHTML = renderDenseTree(roots, byParent, expandedIdSet, entitySummaryByNodeId, showAssignee);
491
+ container.innerHTML = renderDenseTree(roots, byParent, expandedIdSet, entitySummaryByNodeId, showAssignee, entityRowsByNodeId, elementPreviewMode, readonly, selectedId);
369
492
  } else {
370
493
  const htmlParts = roots.map((root) => {
371
494
  const hasChildren = ((byParent.get(root.id) || []).length > 0);
372
495
  return `
373
496
  <div class="root-grid-item ${hasChildren ? 'branch' : 'leaf'}" data-root-item-type="${hasChildren ? 'branch' : 'leaf'}">
374
- ${renderTreeNode(root, byParent, expandedIdSet, entitySummaryByNodeId, showAssignee)}
497
+ ${renderTreeNode(root, byParent, expandedIdSet, entitySummaryByNodeId, showAssignee, entityRowsByNodeId, elementPreviewMode, readonly, selectedId)}
375
498
  </div>
376
499
  `;
377
500
  });
@@ -382,10 +505,17 @@ export function mountWorkTree(targetId, options) {
382
505
  el.addEventListener('click', (e) => {
383
506
  e.stopPropagation();
384
507
  const action = el.getAttribute('data-action');
508
+ if (action === 'entity-preview') {
509
+ const nodeId = el.getAttribute('data-node-id');
510
+ const entityType = el.getAttribute('data-entity-type') || 'all';
511
+ if (nodeId) onSelectEntity?.(nodeId, entityType);
512
+ return;
513
+ }
385
514
  const id = el.getAttribute('data-id');
386
515
  if (!id) return;
387
516
  if (action === 'toggle') onToggleExpand?.(id);
388
517
  if (action === 'add-child') onAddChild?.(id);
518
+ if (action === 'add-diary') onAddDiary?.(id);
389
519
  if (action === 'edit') onEdit?.(id);
390
520
  if (action === 'delete') onDelete?.(id);
391
521
  if (action === 'quick-chat') onQuickChat?.(id, el);
@@ -402,5 +532,113 @@ export function mountWorkTree(targetId, options) {
402
532
  if (renderMode !== 'dense') {
403
533
  applyAdaptiveLeafGridLayout(container);
404
534
  applyAdaptiveRootMixedGridLayout(container);
535
+ return;
405
536
  }
537
+
538
+ let draggingNodeId = '';
539
+ let draggingLaneId = '';
540
+ const dragDropPositionByNodeId = new Map();
541
+ const rowEls = Array.from(container.querySelectorAll('.lane-tree-node-row[data-node-id]'));
542
+ rowEls.forEach((el) => {
543
+ const nodeId = el.getAttribute('data-node-id') || '';
544
+ el.addEventListener('dragstart', (event) => {
545
+ draggingNodeId = nodeId;
546
+ draggingLaneId = '';
547
+ el.classList.add('is-dragging');
548
+ event.stopPropagation();
549
+ event.dataTransfer?.setData('text/plain', nodeId);
550
+ if (event.dataTransfer) event.dataTransfer.effectAllowed = 'move';
551
+ });
552
+ el.addEventListener('dragend', () => {
553
+ draggingNodeId = '';
554
+ el.classList.remove('is-dragging');
555
+ rowEls.forEach((rowEl) => {
556
+ rowEl.classList.remove('drag-over-target');
557
+ rowEl.removeAttribute('data-drop-position');
558
+ });
559
+ dragDropPositionByNodeId.clear();
560
+ });
561
+ el.addEventListener('dragover', (event) => {
562
+ if (!draggingNodeId || draggingNodeId === nodeId) return;
563
+ event.preventDefault();
564
+ if (event.dataTransfer) event.dataTransfer.dropEffect = 'move';
565
+ const rect = el.getBoundingClientRect();
566
+ const offsetY = event.clientY - rect.top;
567
+ const ratio = rect.height > 0 ? offsetY / rect.height : 0.5;
568
+ let dropPosition = 'child';
569
+ if (ratio < 0.25) dropPosition = 'before';
570
+ else if (ratio > 0.75) dropPosition = 'after';
571
+ dragDropPositionByNodeId.set(nodeId, dropPosition);
572
+ el.setAttribute('data-drop-position', dropPosition);
573
+ el.classList.add('drag-over-target');
574
+ });
575
+ el.addEventListener('dragleave', () => {
576
+ el.classList.remove('drag-over-target');
577
+ el.removeAttribute('data-drop-position');
578
+ dragDropPositionByNodeId.delete(nodeId);
579
+ });
580
+ el.addEventListener('drop', (event) => {
581
+ event.preventDefault();
582
+ el.classList.remove('drag-over-target');
583
+ const dropPosition = dragDropPositionByNodeId.get(nodeId) || 'child';
584
+ el.removeAttribute('data-drop-position');
585
+ dragDropPositionByNodeId.delete(nodeId);
586
+ if (!draggingNodeId || draggingNodeId === nodeId) return;
587
+ if (dropPosition === 'before' || dropPosition === 'after') {
588
+ onReorderSiblings?.(draggingNodeId, nodeId, dropPosition);
589
+ } else {
590
+ onMoveNode?.(draggingNodeId, nodeId);
591
+ }
592
+ draggingNodeId = '';
593
+ });
594
+ });
595
+
596
+ const board = container.querySelector('.dense-lane-board');
597
+ board?.addEventListener('dragover', (event) => {
598
+ if (!draggingNodeId) return;
599
+ event.preventDefault();
600
+ });
601
+ board?.addEventListener('drop', (event) => {
602
+ if (!draggingNodeId) return;
603
+ event.preventDefault();
604
+ onMoveNode?.(draggingNodeId, null);
605
+ draggingNodeId = '';
606
+ });
607
+
608
+ const laneEls = Array.from(container.querySelectorAll('.dense-lane[data-root-id]'));
609
+ const laneHandleEls = Array.from(container.querySelectorAll('.dense-lane-drag-handle[data-lane-drag-id]'));
610
+ laneHandleEls.forEach((handle) => {
611
+ const rootId = handle.getAttribute('data-lane-drag-id') || '';
612
+ handle.addEventListener('dragstart', (event) => {
613
+ draggingLaneId = rootId;
614
+ draggingNodeId = '';
615
+ const lane = handle.closest('.dense-lane');
616
+ lane?.classList.add('is-lane-dragging');
617
+ event.dataTransfer?.setData('text/plain', rootId);
618
+ if (event.dataTransfer) event.dataTransfer.effectAllowed = 'move';
619
+ });
620
+ handle.addEventListener('dragend', () => {
621
+ const lane = handle.closest('.dense-lane');
622
+ lane?.classList.remove('is-lane-dragging');
623
+ draggingLaneId = '';
624
+ });
625
+ });
626
+ laneEls.forEach((el) => {
627
+ const rootId = el.getAttribute('data-root-id') || '';
628
+ el.addEventListener('dragover', (event) => {
629
+ if (!draggingLaneId || draggingLaneId === rootId) return;
630
+ event.preventDefault();
631
+ el.classList.add('lane-drag-over-target');
632
+ });
633
+ el.addEventListener('dragleave', () => {
634
+ el.classList.remove('lane-drag-over-target');
635
+ });
636
+ el.addEventListener('drop', (event) => {
637
+ event.preventDefault();
638
+ el.classList.remove('lane-drag-over-target');
639
+ if (!draggingLaneId || draggingLaneId === rootId) return;
640
+ onReorderLanes?.(draggingLaneId, rootId);
641
+ draggingLaneId = '';
642
+ });
643
+ });
406
644
  }
package/package.json CHANGED
@@ -1,11 +1,13 @@
1
1
  {
2
2
  "name": "@qnote/q-ai-note",
3
- "version": "1.0.4",
3
+ "version": "1.0.6",
4
4
  "type": "module",
5
5
  "description": "AI-assisted personal work sandbox and diary system",
6
6
  "main": "dist/server/index.js",
7
7
  "bin": {
8
- "q-ai-note": "dist/cli.js"
8
+ "q-ai-note": "dist/cli.js",
9
+ "q-ai-note-server": "dist/cli.js",
10
+ "q-ai-note-my-server": "dist/cli.js"
9
11
  },
10
12
  "files": [
11
13
  "dist/**/*",
@@ -16,7 +18,8 @@
16
18
  "build": "tsc && node scripts/copy-web-assets.mjs",
17
19
  "start": "PORT=3000 node dist/server/index.js",
18
20
  "test": "vitest run",
19
- "test:e2e": "playwright test",
21
+ "test:e2e": "env -u CI playwright test --grep-invert @ai --workers=1",
22
+ "test:e2e:full": "playwright test",
20
23
  "test:watch": "vitest",
21
24
  "paths:check": "tsx scripts/print-runtime-paths.ts",
22
25
  "prepack": "npm run build"