@yolk_vat-y/dsh-project-memory 0.4.2 → 0.4.4
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/CHANGELOG.md +34 -0
- package/README.md +33 -4
- package/README.zh-CN.md +34 -6
- package/client/client.js +709 -451
- package/client/client.js.map +1 -1
- package/package.json +9 -9
- package/src/client/TaskCommandNode.tsx +14 -22
- package/src/client/TaskComponents.tsx +341 -0
- package/src/client/TaskPanel.module.css +103 -40
- package/src/client/TaskPanel.tsx +104 -381
- package/src/client/locales.ts +6 -0
- package/src/client/task-data-store.ts +136 -0
- package/src/client/task-hooks.ts +169 -0
- package/src/client/task-ui-store.ts +118 -0
- package/src/commands/task-actions.js +9 -7
- package/src/commands/tasks.js +5 -3
- package/src/index.js +4 -3
- package/src/tools/task-tools.js +19 -0
- package/src/client/task-store.ts +0 -192
package/src/client/locales.ts
CHANGED
|
@@ -15,6 +15,9 @@ export const zh = {
|
|
|
15
15
|
'style.mono': '终端',
|
|
16
16
|
'panel.syncing': '同步中…',
|
|
17
17
|
'panel.sync-failed': '同步失败',
|
|
18
|
+
'panel.hints-on': '显示提示信息',
|
|
19
|
+
'panel.hints-off': '隐藏提示信息',
|
|
20
|
+
'drag.cancel': '取消拖拽',
|
|
18
21
|
'panel.empty': '暂无任务',
|
|
19
22
|
'panel.empty-desc': '让模型开始工作并维护 todo 清单后会自动建档',
|
|
20
23
|
'panel.no-session': '还没有会话',
|
|
@@ -53,6 +56,9 @@ export const en = {
|
|
|
53
56
|
'style.mono': 'Mono',
|
|
54
57
|
'panel.syncing': 'Syncing…',
|
|
55
58
|
'panel.sync-failed': 'Sync failed',
|
|
59
|
+
'panel.hints-on': 'Show hints',
|
|
60
|
+
'panel.hints-off': 'Hide hints',
|
|
61
|
+
'drag.cancel': 'Cancel drag',
|
|
56
62
|
'panel.empty': 'No Tasks',
|
|
57
63
|
'panel.empty-desc': 'Tasks are created automatically when the model maintains a todo list',
|
|
58
64
|
'panel.no-session': 'No session yet',
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Task Data Store — 仅管服务端同步的数据(tasks、boundTaskId、archivedCount)
|
|
3
|
+
* - 单向数据流:命令执行结果 → setTasks → 广播给其他标签页
|
|
4
|
+
* - 无 UI 状态,无 localStorage,纯内存 + BroadcastChannel 同步
|
|
5
|
+
*/
|
|
6
|
+
import { useSyncExternalStore } from 'react'
|
|
7
|
+
|
|
8
|
+
export interface TaskStep {
|
|
9
|
+
content: string
|
|
10
|
+
status: 'pending' | 'in_progress' | 'completed'
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export interface TaskFile {
|
|
14
|
+
path: string
|
|
15
|
+
line?: number
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export interface Task {
|
|
19
|
+
id: string
|
|
20
|
+
title: string
|
|
21
|
+
steps: TaskStep[]
|
|
22
|
+
files: TaskFile[]
|
|
23
|
+
lastActiveAt: string
|
|
24
|
+
updatedAt: string
|
|
25
|
+
archived?: boolean
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export interface TaskPayload {
|
|
29
|
+
tasks: Task[]
|
|
30
|
+
boundId: string | null
|
|
31
|
+
archived: number
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function parseTaskPayloadText(text?: string | null): TaskPayload | null {
|
|
35
|
+
if (!text) return null
|
|
36
|
+
let jsonBlock: string | null = null
|
|
37
|
+
const fenced = text.match(/```(?:json)?\s*\n([\s\S]*?)\n```/)
|
|
38
|
+
if (fenced) jsonBlock = fenced[1]
|
|
39
|
+
else {
|
|
40
|
+
const lastBrace = text.lastIndexOf('{')
|
|
41
|
+
if (lastBrace !== -1) jsonBlock = text.slice(lastBrace)
|
|
42
|
+
}
|
|
43
|
+
if (!jsonBlock) return null
|
|
44
|
+
try {
|
|
45
|
+
const data = JSON.parse(jsonBlock)
|
|
46
|
+
if (!Array.isArray(data.tasks)) return null
|
|
47
|
+
return { tasks: data.tasks, boundId: data.boundId ?? null, archived: data.archived ?? 0 }
|
|
48
|
+
} catch {
|
|
49
|
+
return null
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
interface TaskDataState {
|
|
54
|
+
tasks: Task[]
|
|
55
|
+
boundTaskId: string | null
|
|
56
|
+
archivedCount: number
|
|
57
|
+
lastUpdate: number
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const SYNC_CHANNEL = typeof window !== 'undefined' ? new BroadcastChannel('dsh-pm-tasks-data') : null
|
|
61
|
+
|
|
62
|
+
let dataState: TaskDataState = {
|
|
63
|
+
tasks: [],
|
|
64
|
+
boundTaskId: null,
|
|
65
|
+
archivedCount: 0,
|
|
66
|
+
lastUpdate: 0,
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const dataListeners = new Set<() => void>()
|
|
70
|
+
|
|
71
|
+
function setDataState(next: TaskDataState): void {
|
|
72
|
+
dataState = next
|
|
73
|
+
for (const listener of dataListeners) listener()
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function broadcastDataUpdate(tasks: Task[], archivedCount: number): void {
|
|
77
|
+
if (!SYNC_CHANNEL) return
|
|
78
|
+
SYNC_CHANNEL.postMessage({
|
|
79
|
+
type: 'PROJECT_TASKS_UPDATED',
|
|
80
|
+
payload: { tasks, archivedCount },
|
|
81
|
+
})
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
if (SYNC_CHANNEL) {
|
|
85
|
+
SYNC_CHANNEL.onmessage = (event) => {
|
|
86
|
+
const msg = event.data
|
|
87
|
+
if (msg?.type === 'PROJECT_TASKS_UPDATED' && msg.payload) {
|
|
88
|
+
const { tasks, archivedCount } = msg.payload
|
|
89
|
+
setDataState(prev => ({
|
|
90
|
+
...prev,
|
|
91
|
+
tasks: tasks || [],
|
|
92
|
+
archivedCount: archivedCount ?? 0,
|
|
93
|
+
lastUpdate: Date.now(),
|
|
94
|
+
}))
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export const taskDataStore = {
|
|
100
|
+
subscribe(listener: () => void): () => void {
|
|
101
|
+
dataListeners.add(listener)
|
|
102
|
+
return () => { dataListeners.delete(listener) }
|
|
103
|
+
},
|
|
104
|
+
getSnapshot(): TaskDataState {
|
|
105
|
+
return dataState
|
|
106
|
+
},
|
|
107
|
+
actions: {
|
|
108
|
+
setTasks(payload: { tasks: Task[]; boundTaskId: string | null; archivedCount: number }): void {
|
|
109
|
+
const nextTasks = payload.tasks || []
|
|
110
|
+
const nextArchived = payload.archivedCount ?? 0
|
|
111
|
+
setDataState({
|
|
112
|
+
tasks: nextTasks,
|
|
113
|
+
boundTaskId: payload.boundTaskId ?? null,
|
|
114
|
+
archivedCount: nextArchived,
|
|
115
|
+
lastUpdate: Date.now(),
|
|
116
|
+
})
|
|
117
|
+
broadcastDataUpdate(nextTasks, nextArchived)
|
|
118
|
+
},
|
|
119
|
+
reset(): void {
|
|
120
|
+
setDataState({
|
|
121
|
+
tasks: [],
|
|
122
|
+
boundTaskId: null,
|
|
123
|
+
archivedCount: 0,
|
|
124
|
+
lastUpdate: 0,
|
|
125
|
+
})
|
|
126
|
+
},
|
|
127
|
+
},
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
export function useTaskData(): TaskDataState {
|
|
131
|
+
return useSyncExternalStore(taskDataStore.subscribe, taskDataStore.getSnapshot, taskDataStore.getSnapshot)
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
export function useTaskDataActions() {
|
|
135
|
+
return taskDataStore.actions
|
|
136
|
+
}
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Task Panel Hooks — 步骤拖拽交互逻辑(useTaskDrag)。
|
|
3
|
+
* 只依赖 react + task-data-store 类型,不碰数据/UI store。
|
|
4
|
+
*/
|
|
5
|
+
import { useEffect, useRef, useState, useCallback } from 'react'
|
|
6
|
+
import { TaskStep } from './task-data-store.ts'
|
|
7
|
+
|
|
8
|
+
interface DragState {
|
|
9
|
+
fromIndex: number
|
|
10
|
+
stepContent: string
|
|
11
|
+
clientX: number
|
|
12
|
+
clientY: number
|
|
13
|
+
dragging: boolean
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* useTaskDrag — 步骤拖拽重排(绑定任务内,同一任务卡的步骤行间移动)。
|
|
18
|
+
*
|
|
19
|
+
* 交互模型(拖动全程按住鼠标):
|
|
20
|
+
* - 步骤行上按下(左键、非按钮/输入区域)→ 进入预览;
|
|
21
|
+
* - 按住 350ms 不松,或按住后移动超过 6px → 唤起跟随鼠标的浮动幽灵卡片;
|
|
22
|
+
* - 幽灵悬停到本任务卡内的步骤行时显示蓝色插入线(上半=插到该行前,下半=插到该行后);
|
|
23
|
+
* - 松手(mouseup):落在有效位置 → onReorder 落子;落在卡外/别的任务卡/原行
|
|
24
|
+
* (插入自身前后)/按钮输入等控件上 → 取消;
|
|
25
|
+
* - 350ms 内原地松手 = 普通点击,不产生幽灵;Esc → 取消。
|
|
26
|
+
*/
|
|
27
|
+
export function useTaskDrag(
|
|
28
|
+
taskId: string,
|
|
29
|
+
steps: TaskStep[],
|
|
30
|
+
isBound: boolean,
|
|
31
|
+
onReorder: (fromIndex: number, toIndex: number) => void
|
|
32
|
+
) {
|
|
33
|
+
const [dragState, setDragState] = useState<DragState | null>(null)
|
|
34
|
+
const [dropTargetIndex, setDropTargetIndex] = useState<number | null>(null)
|
|
35
|
+
|
|
36
|
+
// 最新值镜像:拖拽期间 window 监听只在“会话开始/结束”时挂载/卸载,
|
|
37
|
+
// 避免每次 mousemove 更新状态导致监听器重挂、连 350ms 定时器都被清掉。
|
|
38
|
+
const dragRef = useRef(dragState)
|
|
39
|
+
dragRef.current = dragState
|
|
40
|
+
const longPressTimer = useRef<number | undefined>(undefined)
|
|
41
|
+
const dragOrigin = useRef<{ x: number; y: number } | null>(null)
|
|
42
|
+
|
|
43
|
+
const clearTimer = useCallback(() => {
|
|
44
|
+
if (longPressTimer.current !== undefined) {
|
|
45
|
+
window.clearTimeout(longPressTimer.current)
|
|
46
|
+
longPressTimer.current = undefined
|
|
47
|
+
}
|
|
48
|
+
}, [])
|
|
49
|
+
|
|
50
|
+
const cancelDrag = useCallback(() => {
|
|
51
|
+
clearTimer()
|
|
52
|
+
dragOrigin.current = null
|
|
53
|
+
document.body.style.userSelect = ''
|
|
54
|
+
setDropTargetIndex(null)
|
|
55
|
+
setDragState(null)
|
|
56
|
+
}, [clearTimer])
|
|
57
|
+
|
|
58
|
+
const startDrag = useCallback((
|
|
59
|
+
fromIndex: number,
|
|
60
|
+
stepContent: string,
|
|
61
|
+
e: React.MouseEvent
|
|
62
|
+
) => {
|
|
63
|
+
if (!isBound || e.button !== 0) return
|
|
64
|
+
if (dragRef.current) return // 已有预览/拖拽会话
|
|
65
|
+
e.preventDefault()
|
|
66
|
+
e.stopPropagation()
|
|
67
|
+
document.body.style.userSelect = 'none'
|
|
68
|
+
dragOrigin.current = { x: e.clientX, y: e.clientY }
|
|
69
|
+
setDropTargetIndex(null)
|
|
70
|
+
setDragState({ fromIndex, stepContent, clientX: e.clientX, clientY: e.clientY, dragging: false })
|
|
71
|
+
clearTimer()
|
|
72
|
+
longPressTimer.current = window.setTimeout(() => {
|
|
73
|
+
longPressTimer.current = undefined
|
|
74
|
+
// 350ms 长按 → 唤起幽灵卡片
|
|
75
|
+
setDragState(prev => (prev ? { ...prev, dragging: true } : null))
|
|
76
|
+
}, 350)
|
|
77
|
+
}, [isBound, clearTimer])
|
|
78
|
+
|
|
79
|
+
/** 计算某行上 y 坐标对应的插入位(行数下标:上半=该行前,下半=该行后);非本任务卡返回 null */
|
|
80
|
+
const dropIndexAt = useCallback((row: Element, clientY: number): number | null => {
|
|
81
|
+
const parentOl = row.closest('ol')
|
|
82
|
+
if (!parentOl || !parentOl.closest(`article[data-task-id="${taskId}"]`)) return null
|
|
83
|
+
const index = Array.from(parentOl.children).indexOf(row)
|
|
84
|
+
const rect = row.getBoundingClientRect()
|
|
85
|
+
return clientY < rect.top + rect.height / 2 ? index : index + 1
|
|
86
|
+
}, [taskId])
|
|
87
|
+
|
|
88
|
+
/** 在 (x,y) 处结算:命中有效落点则重排,否则取消;并结束本次拖拽 */
|
|
89
|
+
const commitDrop = useCallback((x: number, y: number) => {
|
|
90
|
+
const cur = dragRef.current
|
|
91
|
+
if (!cur || !cur.dragging) return
|
|
92
|
+
const el = document.elementFromPoint(x, y) as HTMLElement | null
|
|
93
|
+
// 松手点在按钮/输入等交互控件上:不落子,直接取消
|
|
94
|
+
if (el && !el.closest('button, input, textarea, a')) {
|
|
95
|
+
const row = el.closest('[data-step-row]')
|
|
96
|
+
if (row) {
|
|
97
|
+
const drop = dropIndexAt(row, y)
|
|
98
|
+
if (drop !== null) {
|
|
99
|
+
// 行下标形式的落点:插到目标行前/后;落在自身原位视为取消(不产生空推送)
|
|
100
|
+
const effective = drop > cur.fromIndex ? drop - 1 : drop
|
|
101
|
+
if (effective !== cur.fromIndex) onReorder(cur.fromIndex, drop)
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
cancelDrag()
|
|
106
|
+
}, [dropIndexAt, onReorder, cancelDrag])
|
|
107
|
+
|
|
108
|
+
const active = dragState !== null
|
|
109
|
+
|
|
110
|
+
useEffect(() => {
|
|
111
|
+
if (!active) return
|
|
112
|
+
|
|
113
|
+
const onMouseMove = (e: MouseEvent) => {
|
|
114
|
+
const cur = dragRef.current
|
|
115
|
+
if (!cur) return
|
|
116
|
+
if (!cur.dragging) {
|
|
117
|
+
// 未到 350ms:移动超过阈值视为拖拽开始(不必死等长按)
|
|
118
|
+
const origin = dragOrigin.current
|
|
119
|
+
if (!origin) return
|
|
120
|
+
if (Math.hypot(e.clientX - origin.x, e.clientY - origin.y) < 6) return
|
|
121
|
+
clearTimer()
|
|
122
|
+
setDragState(prev => (prev ? { ...prev, dragging: true, clientX: e.clientX, clientY: e.clientY } : null))
|
|
123
|
+
} else {
|
|
124
|
+
setDragState(prev => (prev ? { ...prev, clientX: e.clientX, clientY: e.clientY } : null))
|
|
125
|
+
}
|
|
126
|
+
const row = document.elementFromPoint(e.clientX, e.clientY)?.closest('[data-step-row]')
|
|
127
|
+
setDropTargetIndex(row ? dropIndexAt(row, e.clientY) : null)
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
const onMouseUp = (e: MouseEvent) => {
|
|
131
|
+
const cur = dragRef.current
|
|
132
|
+
if (!cur) return
|
|
133
|
+
if (cur.dragging) {
|
|
134
|
+
// 拖拽中松手 = 落子/取消
|
|
135
|
+
commitDrop(e.clientX, e.clientY)
|
|
136
|
+
} else {
|
|
137
|
+
// 350ms 内原地松手 = 普通点击
|
|
138
|
+
cancelDrag()
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
const onClick = (e: MouseEvent) => {
|
|
143
|
+
// 兜底:mouseup 发生在窗口外等情况时,下一次点击直接结算
|
|
144
|
+
const cur = dragRef.current
|
|
145
|
+
if (!cur || !cur.dragging) return
|
|
146
|
+
commitDrop(e.clientX, e.clientY)
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
const onKeyDown = (e: KeyboardEvent) => {
|
|
150
|
+
if (e.key === 'Escape') cancelDrag()
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
window.addEventListener('mousemove', onMouseMove)
|
|
154
|
+
window.addEventListener('mouseup', onMouseUp)
|
|
155
|
+
window.addEventListener('click', onClick, true)
|
|
156
|
+
window.addEventListener('keydown', onKeyDown)
|
|
157
|
+
return () => {
|
|
158
|
+
window.removeEventListener('mousemove', onMouseMove)
|
|
159
|
+
window.removeEventListener('mouseup', onMouseUp)
|
|
160
|
+
window.removeEventListener('click', onClick, true)
|
|
161
|
+
window.removeEventListener('keydown', onKeyDown)
|
|
162
|
+
document.body.style.userSelect = ''
|
|
163
|
+
}
|
|
164
|
+
}, [active, taskId, dropIndexAt, onReorder, cancelDrag, commitDrop])
|
|
165
|
+
|
|
166
|
+
useEffect(() => () => clearTimer(), [clearTimer])
|
|
167
|
+
|
|
168
|
+
return { dragState, dropTargetIndex, startDrag, cancelDrag }
|
|
169
|
+
}
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Task UI Store — 仅管本地 UI 状态(closed、minimized、position、expandedIds、theme)
|
|
3
|
+
* - 持久化到 localStorage(key: dsh-pm-task-panel-ui)
|
|
4
|
+
* - 不跨标签页同步(每个标签页独立)
|
|
5
|
+
* - 启动/刷新强制 closed: true,面板默认不显示
|
|
6
|
+
*/
|
|
7
|
+
import { useSyncExternalStore } from 'react'
|
|
8
|
+
|
|
9
|
+
const STORAGE_KEY = 'dsh-pm-task-panel-ui'
|
|
10
|
+
|
|
11
|
+
interface TaskUIState {
|
|
12
|
+
expandedTaskIds: string[]
|
|
13
|
+
panelPosition: { x: number; y: number }
|
|
14
|
+
minimized: boolean
|
|
15
|
+
closed: boolean
|
|
16
|
+
theme: string
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function defaultPosition(): { x: number; y: number } {
|
|
20
|
+
if (typeof window !== 'undefined') {
|
|
21
|
+
return { x: Math.max(16, window.innerWidth - 408), y: 72 }
|
|
22
|
+
}
|
|
23
|
+
return { x: 0, y: 0 }
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* 启动时的初始状态:
|
|
28
|
+
* - closed 恒为 true(不读取 localStorage 的 closed),刷新后面板隐藏;
|
|
29
|
+
* - 其余(展开项/位置/主题)可恢复上次会话偏好,minimized 默认折叠成迷你条。
|
|
30
|
+
*/
|
|
31
|
+
function getDefaultUIState(): TaskUIState {
|
|
32
|
+
if (typeof window !== 'undefined') {
|
|
33
|
+
try {
|
|
34
|
+
const saved = localStorage.getItem(STORAGE_KEY)
|
|
35
|
+
if (saved) {
|
|
36
|
+
const parsed = JSON.parse(saved)
|
|
37
|
+
return {
|
|
38
|
+
expandedTaskIds: Array.isArray(parsed.expandedTaskIds) ? parsed.expandedTaskIds : [],
|
|
39
|
+
panelPosition: parsed.panelPosition ?? defaultPosition(),
|
|
40
|
+
minimized: parsed.minimized !== false,
|
|
41
|
+
closed: true,
|
|
42
|
+
theme: typeof parsed.theme === 'string' ? parsed.theme : 'native',
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
} catch { /* corrupt state — fall through to defaults */ }
|
|
46
|
+
}
|
|
47
|
+
return {
|
|
48
|
+
expandedTaskIds: [],
|
|
49
|
+
panelPosition: defaultPosition(),
|
|
50
|
+
minimized: true,
|
|
51
|
+
closed: true,
|
|
52
|
+
theme: 'native',
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function persistUI(state: TaskUIState): void {
|
|
57
|
+
if (typeof window === 'undefined') return
|
|
58
|
+
try {
|
|
59
|
+
localStorage.setItem(STORAGE_KEY, JSON.stringify(state))
|
|
60
|
+
} catch { /* storage full/unavailable — non-fatal */ }
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
let uiState: TaskUIState = getDefaultUIState()
|
|
64
|
+
const uiListeners = new Set<() => void>()
|
|
65
|
+
|
|
66
|
+
function setUIState(next: TaskUIState): void {
|
|
67
|
+
uiState = next
|
|
68
|
+
persistUI(next)
|
|
69
|
+
for (const listener of uiListeners) listener()
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export const taskUIStore = {
|
|
73
|
+
subscribe(listener: () => void): () => void {
|
|
74
|
+
uiListeners.add(listener)
|
|
75
|
+
return () => { uiListeners.delete(listener) }
|
|
76
|
+
},
|
|
77
|
+
getSnapshot(): TaskUIState {
|
|
78
|
+
return uiState
|
|
79
|
+
},
|
|
80
|
+
actions: {
|
|
81
|
+
toggleTaskExpanded(taskId: string): void {
|
|
82
|
+
const prev = uiState
|
|
83
|
+
const ids = prev.expandedTaskIds.includes(taskId)
|
|
84
|
+
? prev.expandedTaskIds.filter((id) => id !== taskId)
|
|
85
|
+
: [...prev.expandedTaskIds, taskId]
|
|
86
|
+
setUIState({ ...prev, expandedTaskIds: ids })
|
|
87
|
+
},
|
|
88
|
+
expandAll(taskIds: string[]): void {
|
|
89
|
+
setUIState({ ...uiState, expandedTaskIds: taskIds })
|
|
90
|
+
},
|
|
91
|
+
setPanelPosition(pos: { x: number; y: number }): void {
|
|
92
|
+
setUIState({ ...uiState, panelPosition: pos })
|
|
93
|
+
},
|
|
94
|
+
open(): void {
|
|
95
|
+
setUIState({ ...uiState, minimized: false, closed: false })
|
|
96
|
+
},
|
|
97
|
+
minimize(): void {
|
|
98
|
+
setUIState({ ...uiState, minimized: true, closed: false })
|
|
99
|
+
},
|
|
100
|
+
close(): void {
|
|
101
|
+
setUIState({ ...uiState, minimized: true, closed: true })
|
|
102
|
+
},
|
|
103
|
+
setTheme(theme: string): void {
|
|
104
|
+
setUIState({ ...uiState, theme })
|
|
105
|
+
},
|
|
106
|
+
reset(): void {
|
|
107
|
+
setUIState(getDefaultUIState())
|
|
108
|
+
},
|
|
109
|
+
},
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
export function useTaskUI(): TaskUIState {
|
|
113
|
+
return useSyncExternalStore(taskUIStore.subscribe, taskUIStore.getSnapshot, taskUIStore.getSnapshot)
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
export function useTaskUIActions() {
|
|
117
|
+
return taskUIStore.actions
|
|
118
|
+
}
|
|
@@ -25,15 +25,17 @@ function withTaskSnapshot(config, cwd, sid, store, note) {
|
|
|
25
25
|
return { kind: 'success', text: `${human}\n\n\`\`\`json\n${payload}\n\`\`\`` }
|
|
26
26
|
}
|
|
27
27
|
|
|
28
|
-
export function taskCommandDefinition(config) {
|
|
28
|
+
export function taskCommandDefinition(config, ctx) {
|
|
29
29
|
return {
|
|
30
30
|
name: 'task',
|
|
31
31
|
description: '任务面板动作:/task switch <任务id> 切换绑定,/task archive <任务id> 归档,/task unbind 取消当前任务绑定',
|
|
32
32
|
input: { hint: 'switch|archive <任务id> | unbind' },
|
|
33
33
|
handler: (invocation) => {
|
|
34
34
|
try {
|
|
35
|
-
const
|
|
36
|
-
const sid =
|
|
35
|
+
const agent = invocation?.agent
|
|
36
|
+
const sid = agent?.id || agent?.session?.id
|
|
37
|
+
const session = sid && agent?.ctx ? agent.ctx.sessions?.get(sid) : (agent?.session || null)
|
|
38
|
+
const cwd = session?.header?.cwd || agent?.session?.header?.cwd
|
|
37
39
|
const raw = (invocation?.rawInput || '').trim()
|
|
38
40
|
const [verb, taskId, ...rest] = raw.split(/\s+/)
|
|
39
41
|
|
|
@@ -56,8 +58,8 @@ export function taskCommandDefinition(config) {
|
|
|
56
58
|
if (sid) store.removeBinding(sid)
|
|
57
59
|
store.save()
|
|
58
60
|
// 推空 todo/write 清掉输入框上方的宿主任务清单(TaskBridge 空写=清空,不再自动建档)
|
|
59
|
-
if (
|
|
60
|
-
|
|
61
|
+
if (session && typeof session.append === 'function') {
|
|
62
|
+
session.append('todo/write', { todos: [] })
|
|
61
63
|
}
|
|
62
64
|
const note = '已取消当前任务绑定并清空会话任务清单(需要时可让模型重新规划)'
|
|
63
65
|
return withTaskSnapshot(config, cwd, sid, store, note)
|
|
@@ -116,7 +118,7 @@ export function taskCommandDefinition(config) {
|
|
|
116
118
|
task.lastActiveAt = task.updatedAt
|
|
117
119
|
store.save()
|
|
118
120
|
if (shouldAdoptToHost(config)) {
|
|
119
|
-
adoptStepsToSession(
|
|
121
|
+
adoptStepsToSession(session, task)
|
|
120
122
|
}
|
|
121
123
|
const note = `已更新「${task.title}」步骤(${norm.length} 条)`
|
|
122
124
|
return withTaskSnapshot(config, cwd, sid, store, note)
|
|
@@ -134,7 +136,7 @@ export function taskCommandDefinition(config) {
|
|
|
134
136
|
store.save()
|
|
135
137
|
// 反向接管:切换成功后把任务步骤推成宿主 todo/write(dsh 清单跟随)
|
|
136
138
|
if (shouldAdoptToHost(config)) {
|
|
137
|
-
adoptStepsToSession(
|
|
139
|
+
adoptStepsToSession(session, task)
|
|
138
140
|
}
|
|
139
141
|
const note = `已切换绑定: ${describeTask(task)}${sid ? '' : '(拿不到会话 id,未持久绑定)'}`
|
|
140
142
|
return withTaskSnapshot(config, cwd, sid, store, note)
|
package/src/commands/tasks.js
CHANGED
|
@@ -69,14 +69,16 @@ export function renderTaskSnapshot(config, cwd, sid) {
|
|
|
69
69
|
return { kind: 'success', text: `${humanText}\n\n\`\`\`json\n${payload}\n\`\`\`` }
|
|
70
70
|
}
|
|
71
71
|
|
|
72
|
-
export function tasksCommandDefinition(config) {
|
|
72
|
+
export function tasksCommandDefinition(config, ctx) {
|
|
73
73
|
return {
|
|
74
74
|
name: 'tasks',
|
|
75
75
|
description: '查看本项目的任务清单(几套任务、进度、涉及文件、当前绑定)',
|
|
76
76
|
handler: (invocation) => {
|
|
77
77
|
try {
|
|
78
|
-
const
|
|
79
|
-
const sid =
|
|
78
|
+
const agent = invocation?.agent
|
|
79
|
+
const sid = agent?.id || agent?.session?.id
|
|
80
|
+
const session = sid && agent?.ctx ? agent.ctx.sessions?.get(sid) : (agent?.session || null)
|
|
81
|
+
const cwd = session?.header?.cwd || agent?.session?.header?.cwd
|
|
80
82
|
return renderTaskSnapshot(config, cwd, sid)
|
|
81
83
|
} catch (err) {
|
|
82
84
|
return { kind: 'error', text: `[tasks] ${err?.message || err}` }
|
package/src/index.js
CHANGED
|
@@ -10,7 +10,7 @@ import { WatchManager } from './watch.js'
|
|
|
10
10
|
import { setupLazyIndexing } from './lazy.js'
|
|
11
11
|
import { initTypeScript } from './enhancer.js'
|
|
12
12
|
import { setupTaskbridge } from './setup/taskbridge.js'
|
|
13
|
-
import { listTasksTool, selectTaskTool, archiveTaskTool } from './tools/task-tools.js'
|
|
13
|
+
import { listTasksTool, selectTaskTool, archiveTaskTool, showTaskPanelTool } from './tools/task-tools.js'
|
|
14
14
|
import { tasksCommandDefinition } from './commands/tasks.js'
|
|
15
15
|
import { taskCommandDefinition } from './commands/task-actions.js'
|
|
16
16
|
|
|
@@ -63,12 +63,13 @@ export function apply(ctx, config) {
|
|
|
63
63
|
ctx.tools.register(listTasksTool(config))
|
|
64
64
|
ctx.tools.register(selectTaskTool(config))
|
|
65
65
|
ctx.tools.register(archiveTaskTool(config))
|
|
66
|
+
ctx.tools.register(showTaskPanelTool(config))
|
|
66
67
|
|
|
67
68
|
// /tasks、/task 用户命令(宿主 commands 服务存在时注册,feature-detect 降级)
|
|
68
69
|
try {
|
|
69
70
|
ctx.inject(['commands'], (commandsCtx) => {
|
|
70
|
-
commandsCtx.commands.register(tasksCommandDefinition(config))
|
|
71
|
-
commandsCtx.commands.register(taskCommandDefinition(config))
|
|
71
|
+
commandsCtx.commands.register(tasksCommandDefinition(config, ctx))
|
|
72
|
+
commandsCtx.commands.register(taskCommandDefinition(config, ctx))
|
|
72
73
|
})
|
|
73
74
|
} catch (err) {
|
|
74
75
|
console.error(`[dsh-project-memory] /tasks,/task registration skipped: ${err.message}`)
|
package/src/tools/task-tools.js
CHANGED
|
@@ -145,6 +145,25 @@ export function selectTaskTool(config) {
|
|
|
145
145
|
})
|
|
146
146
|
}
|
|
147
147
|
|
|
148
|
+
export function showTaskPanelTool(config) {
|
|
149
|
+
return defineTool({
|
|
150
|
+
name: 'show_task_panel',
|
|
151
|
+
description:
|
|
152
|
+
'Show the task panel in the UI. Call this when the user asks to see the task list or when you want to display the task panel to the user.',
|
|
153
|
+
parameters: {},
|
|
154
|
+
output: { schema: { type: 'string' }, render: (_a, v) => [{ type: 'text', text: v }] },
|
|
155
|
+
async execute(_args, exec) {
|
|
156
|
+
const ctx = exec?.ctx
|
|
157
|
+
if (!ctx) return '无法获取上下文'
|
|
158
|
+
// 通过 BroadcastChannel 通知客户端打开面板
|
|
159
|
+
try {
|
|
160
|
+
ctx.events?.emit?.('dsh:task-panel:show', {})
|
|
161
|
+
} catch { /* ignore */ }
|
|
162
|
+
return '已请求打开任务面板。'
|
|
163
|
+
},
|
|
164
|
+
})
|
|
165
|
+
}
|
|
166
|
+
|
|
148
167
|
export function archiveTaskTool(config) {
|
|
149
168
|
return defineTool({
|
|
150
169
|
name: 'archive_task',
|