@yolk_vat-y/dsh-project-memory 0.5.6 → 0.5.8
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 +153 -0
- package/README.md +37 -34
- package/README.zh-CN.md +35 -34
- package/client/client.js +458 -107
- package/client/client.js.map +1 -1
- package/package.json +3 -6
- package/src/audit.js +63 -0
- package/src/auto-inject.js +447 -286
- package/src/chunker.js +14 -6
- package/src/client/MemoryView.tsx +12 -4
- package/src/client/TaskCommandNode.tsx +1 -1
- package/src/client/TaskComponents.tsx +3 -2
- package/src/client/TaskPanel.tsx +13 -13
- package/src/client/client.ts +93 -21
- package/src/client/icons.ts +63 -0
- package/src/client/locales.ts +11 -0
- package/src/client/session-id.js +88 -0
- package/src/client/slash.ts +184 -0
- package/src/commands/insight-actions.js +31 -16
- package/src/commands/invocation.js +36 -0
- package/src/commands/task-actions.js +36 -40
- package/src/commands/tasks.js +18 -13
- package/src/commands/workflow.js +54 -0
- package/src/enhancer.js +11 -2
- package/src/index-pipeline.js +119 -0
- package/src/index.js +24 -8
- package/src/insight-store.js +96 -28
- package/src/lazy.js +15 -46
- package/src/link.js +10 -1
- package/src/parsers/pdfjs-parser.js +2 -4
- package/src/project-profile.js +12 -5
- package/src/readiness.js +27 -3
- package/src/recall.js +28 -8
- package/src/setup/taskbridge.js +11 -9
- package/src/store.js +56 -17
- package/src/symbols.js +58 -19
- package/src/tools/forget.js +2 -1
- package/src/tools/index-doc.js +4 -1
- package/src/tools/index-repo.js +33 -86
- package/src/tools/lesson-tools.js +2 -1
- package/src/tools/query-memory.js +29 -4
- package/src/tools/remember.js +3 -1
- package/src/tools/task-tools.js +4 -1
- package/src/tools/watch-repo.js +9 -5
- package/src/util/fs.js +14 -0
- package/src/util/session-cache.js +72 -0
- package/src/watch.js +39 -81
package/src/chunker.js
CHANGED
|
@@ -20,17 +20,25 @@ export function chunkText(text, chunkChars = 3000, maxChunks = 40) {
|
|
|
20
20
|
|
|
21
21
|
const chunks = []
|
|
22
22
|
for (const section of sections) {
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
23
|
+
const rawBlock = section.lines.join('\n')
|
|
24
|
+
if (!rawBlock.trim()) continue
|
|
25
|
+
// `.trim()` 会吃掉段首的空白与换行——把它们算进行号,否则整段的引用行号会偏低。
|
|
26
|
+
const lead = rawBlock.match(/^\s*/)[0]
|
|
27
|
+
let line = section.line + (lead.match(/\n/g) || []).length
|
|
28
|
+
let block = rawBlock.trim()
|
|
26
29
|
while (block.length > chunkChars) {
|
|
27
30
|
let splitAt = block.lastIndexOf('\n\n', chunkChars)
|
|
28
31
|
if (splitAt < chunkChars * 0.5) splitAt = block.lastIndexOf(' ', chunkChars)
|
|
29
32
|
if (splitAt < chunkChars * 0.5) splitAt = chunkChars
|
|
30
|
-
const
|
|
33
|
+
const rawPart = block.slice(0, splitAt)
|
|
34
|
+
const part = rawPart.trim()
|
|
31
35
|
if (part) chunks.push({ title: section.title, text: part, line })
|
|
32
|
-
|
|
33
|
-
|
|
36
|
+
// 行号按**原始**切片里的换行数推进,再加上被下一次 trim 吃掉的段首换行。
|
|
37
|
+
// 旧写法 `part.split('\n').length` 每次跨行切分都要多算一行,误差会累积。
|
|
38
|
+
const rest = block.slice(splitAt)
|
|
39
|
+
const nextLead = rest.match(/^\s*/)[0]
|
|
40
|
+
line += (rawPart.match(/\n/g) || []).length + (nextLead.match(/\n/g) || []).length
|
|
41
|
+
block = rest.trim()
|
|
34
42
|
if (chunks.length >= maxChunks) break
|
|
35
43
|
}
|
|
36
44
|
if (block) {
|
|
@@ -120,6 +120,14 @@ export function MemoryView({
|
|
|
120
120
|
const lastRunRef = useRef(0)
|
|
121
121
|
|
|
122
122
|
const refresh = useCallback(async (force = false): Promise<void> => {
|
|
123
|
+
// 没有活跃会话时不发命令、也不报错:面板顶部已经显示「还没有会话」,
|
|
124
|
+
// 再报一次"同步失败"会让人以为记忆库坏了(0.1.7 上会话快照结构变化时就是这个表象)。
|
|
125
|
+
if (!sessionId) {
|
|
126
|
+
setPayload(null)
|
|
127
|
+
setError(null)
|
|
128
|
+
setLoading(false)
|
|
129
|
+
return
|
|
130
|
+
}
|
|
123
131
|
if (inflightRef.current) return
|
|
124
132
|
const now = Date.now()
|
|
125
133
|
if (!force && now - lastRunRef.current < 800) return
|
|
@@ -128,7 +136,7 @@ export function MemoryView({
|
|
|
128
136
|
setLoading(true)
|
|
129
137
|
setError(null)
|
|
130
138
|
try {
|
|
131
|
-
const res = await runLine(`/insight list ${scope}`)
|
|
139
|
+
const res = await runLine(`/tasks insight list ${scope}`)
|
|
132
140
|
if (!res.ok) {
|
|
133
141
|
setError(res.text)
|
|
134
142
|
setPayload(null)
|
|
@@ -152,7 +160,7 @@ export function MemoryView({
|
|
|
152
160
|
setBusy(true)
|
|
153
161
|
setError(null)
|
|
154
162
|
try {
|
|
155
|
-
const line = `/insight ${action} ${scope} ${id}${action === 'demote' && scope === 'project' && boundTaskId ? ` ${boundTaskId}` : ''}`
|
|
163
|
+
const line = `/tasks insight ${action} ${scope} ${id}${action === 'demote' && scope === 'project' && boundTaskId ? ` ${boundTaskId}` : ''}`
|
|
156
164
|
const res = await runLine(line)
|
|
157
165
|
if (!res.ok) setError(res.text)
|
|
158
166
|
else void refresh(true)
|
|
@@ -230,8 +238,8 @@ export function MemoryView({
|
|
|
230
238
|
fields.trigger = { keywords: form.trigger.split(/[,,]/).map((s) => s.trim()).filter(Boolean) }
|
|
231
239
|
}
|
|
232
240
|
const line = editingId
|
|
233
|
-
? `/insight edit ${scope} ${editingId} ${JSON.stringify(fields)}`
|
|
234
|
-
: `/insight save ${scope} ${JSON.stringify(fields)}`
|
|
241
|
+
? `/tasks insight edit ${scope} ${editingId} ${JSON.stringify(fields)}`
|
|
242
|
+
: `/tasks insight save ${scope} ${JSON.stringify(fields)}`
|
|
235
243
|
const res = await runLine(line)
|
|
236
244
|
if (!res.ok) {
|
|
237
245
|
setError(res.text)
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* 目的:大 JSON 载荷绝不整段渲染进对话(否则切视图会刷屏)。
|
|
5
5
|
*/
|
|
6
6
|
import { useEffect, useRef } from 'react'
|
|
7
|
-
import { IconFolderOpenOutline16 } from '
|
|
7
|
+
import { IconFolderOpenOutline16 } from './icons.ts'
|
|
8
8
|
import { parseTaskPayloadText, taskDataStore } from './task-data-store.ts'
|
|
9
9
|
import { parseInsightPayloadText } from './MemoryView.tsx'
|
|
10
10
|
import { taskUIStore } from './task-ui-store.ts'
|
|
@@ -3,14 +3,15 @@
|
|
|
3
3
|
* 纯渲染 + 卡片局部行内编辑/拖拽状态,动作经 props 回调交给容器(TaskPanel.tsx)。
|
|
4
4
|
*/
|
|
5
5
|
import { useEffect, useLayoutEffect, useRef, useState } from 'react'
|
|
6
|
+
import { Button } from '@deepseek-ai/dsh-client-ui-primitives'
|
|
7
|
+
// 图标走兼容层:0.1.5 用 IconXxx14/16,0.1.7+ 改成 IconXxxRegular + size prop
|
|
6
8
|
import {
|
|
7
|
-
Button,
|
|
8
9
|
IconChevronDownOutline14,
|
|
9
10
|
IconChevronUpOutline14,
|
|
10
11
|
IconCheckOutline16,
|
|
11
12
|
IconPlayOutline16,
|
|
12
13
|
IconFolderOpenOutline16,
|
|
13
|
-
} from '
|
|
14
|
+
} from './icons.ts'
|
|
14
15
|
import { createTranslate } from './locales.ts'
|
|
15
16
|
import { useTaskDrag } from './task-hooks.ts'
|
|
16
17
|
import { type Task, type TaskInsight, type TaskStep } from './task-data-store.ts'
|
package/src/client/TaskPanel.tsx
CHANGED
|
@@ -4,18 +4,20 @@
|
|
|
4
4
|
* Presentational 组件在 TaskComponents.tsx
|
|
5
5
|
*/
|
|
6
6
|
import { Component, useEffect, useRef, useState, type ReactNode } from 'react'
|
|
7
|
+
import { Button } from '@deepseek-ai/dsh-client-ui-primitives'
|
|
8
|
+
// 图标走兼容层:0.1.5 用 IconXxx14/16,0.1.7+ 改成 IconXxxRegular + size prop
|
|
7
9
|
import {
|
|
8
|
-
Button,
|
|
9
10
|
IconChevronDownOutline14,
|
|
10
11
|
IconCloseOutline16,
|
|
11
12
|
IconFolderOpenOutline16,
|
|
12
13
|
IconQuestionOutline14,
|
|
13
|
-
} from '
|
|
14
|
+
} from './icons.ts'
|
|
14
15
|
import { createTranslate, zh, en } from './locales.ts'
|
|
15
16
|
import { useTaskData, useTaskDataActions, taskDataStore, parseTaskPayloadText, type TaskStep } from './task-data-store.ts'
|
|
16
17
|
import { useTaskUI, useTaskUIActions } from './task-ui-store.ts'
|
|
17
18
|
import { MiniBar, TaskCard } from './TaskComponents.tsx'
|
|
18
19
|
import { MemoryView, type InsightScope } from './MemoryView.tsx'
|
|
20
|
+
import { pickSessionId } from './session-id.js'
|
|
19
21
|
import css from './TaskPanel.module.css'
|
|
20
22
|
|
|
21
23
|
const NS = 'dsh-project-memory'
|
|
@@ -36,11 +38,9 @@ function useSessionId(ctx: any): string | null {
|
|
|
36
38
|
if (!list || typeof list.subscribe !== 'function') return
|
|
37
39
|
return list.subscribe(() => force((n) => n + 1))
|
|
38
40
|
}, [ctx])
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
const first = Array.isArray(snap.items) ? snap.items.find((s: any) => !s.blank) ?? snap.items[0] : undefined
|
|
43
|
-
return first?.sessionId ?? null
|
|
41
|
+
// 快照结构在 dsh 0.1.7 变过(current/items → ids/byId):解析与兼容都在 session-id.js,
|
|
42
|
+
// 那里有单测;这里只负责订阅与取值。
|
|
43
|
+
return pickSessionId(ctx?.sessions?.list?.getSnapshot?.())
|
|
44
44
|
}
|
|
45
45
|
|
|
46
46
|
class PanelErrorBoundary extends Component<{ children: ReactNode }, { failed: boolean }> {
|
|
@@ -134,7 +134,7 @@ function TaskPanelView({ ctx }: { ctx: any }) {
|
|
|
134
134
|
await runLine('/tasks')
|
|
135
135
|
const newBoundId = taskDataStore.getSnapshot().boundTaskId
|
|
136
136
|
if (newBoundId && newBoundId !== prevBoundId) {
|
|
137
|
-
await runLine(`/
|
|
137
|
+
await runLine(`/tasks switch ${newBoundId}`)
|
|
138
138
|
}
|
|
139
139
|
} finally {
|
|
140
140
|
setSyncing(false)
|
|
@@ -145,7 +145,7 @@ function TaskPanelView({ ctx }: { ctx: any }) {
|
|
|
145
145
|
if (syncing) return
|
|
146
146
|
setSyncing(true)
|
|
147
147
|
try {
|
|
148
|
-
await runLine(`/
|
|
148
|
+
await runLine(`/tasks ${verb} ${taskId}`)
|
|
149
149
|
} finally {
|
|
150
150
|
setSyncing(false)
|
|
151
151
|
}
|
|
@@ -156,7 +156,7 @@ function TaskPanelView({ ctx }: { ctx: any }) {
|
|
|
156
156
|
if (syncing) return
|
|
157
157
|
setSyncing(true)
|
|
158
158
|
try {
|
|
159
|
-
await runLine(`/insight ${action} task ${id}`)
|
|
159
|
+
await runLine(`/tasks insight ${action} task ${id}`)
|
|
160
160
|
await refresh()
|
|
161
161
|
} finally {
|
|
162
162
|
setSyncing(false)
|
|
@@ -164,7 +164,7 @@ function TaskPanelView({ ctx }: { ctx: any }) {
|
|
|
164
164
|
}
|
|
165
165
|
|
|
166
166
|
const pushSteps = (taskId: string, steps: TaskStep[]): void => {
|
|
167
|
-
void runLine(`/
|
|
167
|
+
void runLine(`/tasks todos ${JSON.stringify(steps.map((s) => ({ content: s.content, status: s.status })))}`)
|
|
168
168
|
}
|
|
169
169
|
|
|
170
170
|
const cycleStatus = (taskId: string, index: number) => {
|
|
@@ -206,7 +206,7 @@ function TaskPanelView({ ctx }: { ctx: any }) {
|
|
|
206
206
|
if (!task) return
|
|
207
207
|
const same = task.title === trimmed
|
|
208
208
|
if (same || !trimmed) return
|
|
209
|
-
void runLine(`/
|
|
209
|
+
void runLine(`/tasks rename ${taskId} ${JSON.stringify(trimmed)}`)
|
|
210
210
|
}
|
|
211
211
|
|
|
212
212
|
const expand = () => {
|
|
@@ -421,7 +421,7 @@ function TaskPanelView({ ctx }: { ctx: any }) {
|
|
|
421
421
|
expanded={expanded}
|
|
422
422
|
onToggleExpand={() => uiActions.toggleTaskExpanded(task.id)}
|
|
423
423
|
onSwitch={() => handleAction('switch', task.id)}
|
|
424
|
-
onUnbind={() => runLine('/
|
|
424
|
+
onUnbind={() => runLine('/tasks unbind')}
|
|
425
425
|
onArchive={() => handleAction('archive', task.id)}
|
|
426
426
|
onRename={(value) => commitTitle(task.id, value)}
|
|
427
427
|
onEditStep={(index, value) => commitStepText(task.id, index, value)}
|
package/src/client/client.ts
CHANGED
|
@@ -10,10 +10,14 @@
|
|
|
10
10
|
*
|
|
11
11
|
* 只依赖宿主 seed 提供的模块(react / dsh-client-ui-primitives),
|
|
12
12
|
* 数据经 remote.commands.execute 执行 /tasks 命令获取 JSON 快照。
|
|
13
|
+
*
|
|
14
|
+
* 另有一个可选增强:自建的 `/` 菜单源(slash.ts),把三条命令收进一个带图标和
|
|
15
|
+
* 小节标题的「工作流」组。它**整体是可选的**——宿主没有 slash 触发器服务时静默跳过。
|
|
13
16
|
*/
|
|
14
17
|
import { createElement } from 'react'
|
|
15
18
|
import { TaskPanelEntry } from './TaskPanel.tsx'
|
|
16
19
|
import { TaskCommandNode } from './TaskCommandNode.tsx'
|
|
20
|
+
import { createSlashSource } from './slash.ts'
|
|
17
21
|
|
|
18
22
|
const NS = 'dsh-project-memory'
|
|
19
23
|
|
|
@@ -22,32 +26,100 @@ export const name = NS
|
|
|
22
26
|
/** Required client services: slots registry, session scopes, commands remote (data 通道). */
|
|
23
27
|
export const inject = ['slots', 'sessions', 'remote', 'remote.commands', 'locale']
|
|
24
28
|
|
|
29
|
+
/**
|
|
30
|
+
* 执行一条命令行,映射成 composer 的 SubmitOutcome。
|
|
31
|
+
* 与 TaskPanel 走同一个 remote.commands.execute 通道(同样的返回信封)。
|
|
32
|
+
* @param commands - ctx.remote.commands
|
|
33
|
+
* @param session - 会话投影(只读 sessionId)
|
|
34
|
+
* @param line - 完整命令行(含前导斜杠)
|
|
35
|
+
* @param attachments - 提交附件(菜单路径恒为空)
|
|
36
|
+
* @returns {kind:'success'|'error', text?}
|
|
37
|
+
*/
|
|
38
|
+
async function runCommand(
|
|
39
|
+
commands: any,
|
|
40
|
+
session: any,
|
|
41
|
+
line: string,
|
|
42
|
+
attachments: readonly unknown[] = [],
|
|
43
|
+
): Promise<{ kind: 'success' | 'error'; text?: string }> {
|
|
44
|
+
const sessionId = session?.sessionId
|
|
45
|
+
if (typeof sessionId !== 'string' || !commands || typeof commands.execute !== 'function') {
|
|
46
|
+
return { kind: 'error', text: `no session / commands service for ${line}` }
|
|
47
|
+
}
|
|
48
|
+
const response: any = await commands.execute(sessionId, line, [...attachments])
|
|
49
|
+
const envelope = response as { ok?: boolean; error?: any; value?: any } | null | undefined
|
|
50
|
+
if (envelope && typeof envelope === 'object' && envelope.ok === false) {
|
|
51
|
+
return { kind: 'error', text: `command.execute failed: ${envelope.error?.code ?? 'unknown'}` }
|
|
52
|
+
}
|
|
53
|
+
const execution = envelope && typeof envelope === 'object' && 'value' in envelope ? envelope.value : envelope
|
|
54
|
+
const result = execution?.result ?? execution
|
|
55
|
+
if (result?.kind === 'error') return { kind: 'error', text: result.text ?? `${line} failed` }
|
|
56
|
+
return { kind: 'success' }
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* 注册自建的 `/` 菜单源(见 slash.ts)。
|
|
61
|
+
*
|
|
62
|
+
* 整段都是**可选增强**:宿主没有 `inputTriggers` 服务、或该服务换了契约时,绝不能因此
|
|
63
|
+
* 让整个 client 插件挂掉——那会连任务面板一起消失。`inputTriggers` 因此不进顶层 `inject`
|
|
64
|
+
* (顶层 inject 未满足时 cordis 根本不会调用 apply),而是走嵌套 inject + 两层 try/catch。
|
|
65
|
+
* @param ctx - client 根上下文
|
|
66
|
+
*/
|
|
67
|
+
function registerSlashSource(ctx: any): void {
|
|
68
|
+
if (typeof ctx?.inject !== 'function') {
|
|
69
|
+
console.warn(`[${NS}] host has no ctx.inject — slash menu group disabled`)
|
|
70
|
+
return
|
|
71
|
+
}
|
|
72
|
+
try {
|
|
73
|
+
ctx.inject(['inputTriggers', 'sessions', 'remote.commands'], (scope: any) => {
|
|
74
|
+
try {
|
|
75
|
+
const inputTriggers = scope?.inputTriggers
|
|
76
|
+
if (!inputTriggers || typeof inputTriggers.registerSource !== 'function') {
|
|
77
|
+
console.warn(`[${NS}] host has no inputTriggers.registerSource — slash menu group disabled`)
|
|
78
|
+
return
|
|
79
|
+
}
|
|
80
|
+
const source = createSlashSource(scope, {
|
|
81
|
+
sessions: scope.sessions,
|
|
82
|
+
run: (session, line, attachments) => runCommand(scope?.remote?.commands, session, line, attachments),
|
|
83
|
+
})
|
|
84
|
+
scope.effect(() => inputTriggers.registerSource(source), 'dsh-project-memory: slash source')
|
|
85
|
+
} catch (err) {
|
|
86
|
+
console.warn(`[${NS}] slash source registration failed:`, err)
|
|
87
|
+
}
|
|
88
|
+
})
|
|
89
|
+
} catch (err) {
|
|
90
|
+
console.warn(`[${NS}] slash source injection failed:`, err)
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
25
94
|
export function apply(ctx: any): void {
|
|
26
95
|
const slots = ctx?.slots
|
|
27
96
|
if (!slots || typeof slots.inject !== 'function') {
|
|
28
97
|
console.warn(`[${NS}] host has no slots service — task panel disabled`)
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
() => createElement(TaskPanelEntry, { ctx })
|
|
39
|
-
))
|
|
40
|
-
|
|
41
|
-
// 会话内 /tasks、/task、/insight 命令节点:按命令名 key 接管渲染,
|
|
42
|
-
// 替换内置大段文本卡片(文本含 JSON 载荷,只有面板需要它),避免载荷刷屏对话。
|
|
43
|
-
const nodeKeys = ['tasks', 'task', 'insight']
|
|
44
|
-
for (const key of nodeKeys) {
|
|
45
|
-
slots.inject('conversation.chat.commandview', () => slots.register(
|
|
46
|
-
{ name: 'conversation.chat.commandview', key },
|
|
47
|
-
TaskCommandNode
|
|
98
|
+
} else {
|
|
99
|
+
try {
|
|
100
|
+
slots.inject('shell.overlay', () => slots.register(
|
|
101
|
+
{
|
|
102
|
+
name: 'shell.overlay',
|
|
103
|
+
id: 'dsh-project-memory-task-panel',
|
|
104
|
+
order: 100, // 浮层顺序:低于 toast/弹窗即可,越高越靠后渲染
|
|
105
|
+
},
|
|
106
|
+
() => createElement(TaskPanelEntry, { ctx })
|
|
48
107
|
))
|
|
108
|
+
|
|
109
|
+
// 会话内命令节点:按命令名 key 接管渲染,替换内置大段文本卡片
|
|
110
|
+
// (文本含 JSON 载荷,只有面板需要它),避免载荷刷屏对话。
|
|
111
|
+
// 合并后只剩一条用户命令 /tasks(子动词都记在同一个命令名下)。
|
|
112
|
+
const nodeKeys = ['tasks']
|
|
113
|
+
for (const key of nodeKeys) {
|
|
114
|
+
slots.inject('conversation.chat.commandview', () => slots.register(
|
|
115
|
+
{ name: 'conversation.chat.commandview', key },
|
|
116
|
+
TaskCommandNode
|
|
117
|
+
))
|
|
118
|
+
}
|
|
119
|
+
} catch (err) {
|
|
120
|
+
console.warn(`[${NS}] task panel registration failed:`, err)
|
|
49
121
|
}
|
|
50
|
-
} catch (err) {
|
|
51
|
-
console.warn(`[${NS}] task panel registration failed:`, err)
|
|
52
122
|
}
|
|
123
|
+
|
|
124
|
+
registerSlashSource(ctx)
|
|
53
125
|
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 宿主图标兼容层(dsh 0.1.5 ↔ 0.1.7+)。
|
|
3
|
+
*
|
|
4
|
+
* 命名契约变过一次:
|
|
5
|
+
* - dsh 0.1.5(含 0.1.5-rc.x):尺寸写进名字,如 `IconFolderOpenOutline16`;
|
|
6
|
+
* - dsh 0.1.7+:去掉尺寸后缀、改为字重后缀,如 `IconFolderOpenOutlineRegular` /
|
|
7
|
+
* `IconFolderOpenOutlineMedium`,尺寸改由 `size` prop 传入。
|
|
8
|
+
* ui-primitives 是 web shell 的冻结 seed 模块(不是插件行),插件无法固定它,
|
|
9
|
+
* 同一份产物会拿到宿主那一版的导出;直接按某一版的名字解构会在另一版上得到
|
|
10
|
+
* `undefined`,React 渲染时抛 "Element type is invalid" 把面板整个打崩。
|
|
11
|
+
*
|
|
12
|
+
* 因此这里按旧名导出同名组件,运行时在两个名字里取存在的那个,并把旧名里的
|
|
13
|
+
* 尺寸作为默认 `size`(两版的图标都接受 `size` prop,0.1.5 的默认值正是旧名后缀)。
|
|
14
|
+
* 两版都没有该名字时降级为空组件并告警,而不是让面板崩溃。
|
|
15
|
+
*/
|
|
16
|
+
import { createElement, type ComponentType } from 'react'
|
|
17
|
+
import * as primitives from '@deepseek-ai/dsh-client-ui-primitives'
|
|
18
|
+
|
|
19
|
+
type IconProps = { size?: number; className?: string }
|
|
20
|
+
type IconComponent = ComponentType<IconProps>
|
|
21
|
+
|
|
22
|
+
const table = primitives as unknown as Record<string, unknown>
|
|
23
|
+
|
|
24
|
+
function resolveIcon(names: readonly string[]): IconComponent | null {
|
|
25
|
+
for (const name of names) {
|
|
26
|
+
const candidate = table[name]
|
|
27
|
+
if (typeof candidate === 'function' || (typeof candidate === 'object' && candidate !== null)) {
|
|
28
|
+
return candidate as IconComponent
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
return null
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function icon(legacyName: string, currentName: string, defaultSize: number): IconComponent {
|
|
35
|
+
const impl = resolveIcon([legacyName, currentName])
|
|
36
|
+
if (impl === null) {
|
|
37
|
+
console.warn(
|
|
38
|
+
`[dsh-project-memory] host ui-primitives exports neither ${legacyName} nor ${currentName}; that icon renders nothing`,
|
|
39
|
+
)
|
|
40
|
+
return function MissingIcon() {
|
|
41
|
+
return null
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
function CompatIcon({ size = defaultSize, ...rest }: IconProps) {
|
|
45
|
+
return createElement(impl as IconComponent, { size, ...rest })
|
|
46
|
+
}
|
|
47
|
+
CompatIcon.displayName = legacyName
|
|
48
|
+
return CompatIcon
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export const IconChevronDownOutline14 = icon('IconChevronDownOutline14', 'IconChevronDownOutlineRegular', 14)
|
|
52
|
+
export const IconChevronUpOutline14 = icon('IconChevronUpOutline14', 'IconChevronUpOutlineRegular', 14)
|
|
53
|
+
export const IconQuestionOutline14 = icon('IconQuestionOutline14', 'IconQuestionOutlineRegular', 14)
|
|
54
|
+
export const IconCloseOutline16 = icon('IconCloseOutline16', 'IconCloseOutlineRegular', 16)
|
|
55
|
+
export const IconFolderOpenOutline16 = icon('IconFolderOpenOutline16', 'IconFolderOpenOutlineRegular', 16)
|
|
56
|
+
export const IconCheckOutline16 = icon('IconCheckOutline16', 'IconCheckOutlineRegular', 16)
|
|
57
|
+
export const IconPlayOutline16 = icon('IconPlayOutline16', 'IconPlayOutlineRegular', 16)
|
|
58
|
+
|
|
59
|
+
// `/` 菜单行图标。旧名的尺寸后缀两版并不一致(有的是 14 有的是 16),
|
|
60
|
+
// 所以这里的默认 size 只影响"宿主两版都没有该名字"的降级路径——正常路径由宿主自己决定。
|
|
61
|
+
export const IconChecklistOutline16 = icon('IconChecklistOutline14', 'IconChecklistOutlineRegular', 14)
|
|
62
|
+
export const IconLightOutline16 = icon('IconLightOutline16', 'IconLightOutlineRegular', 16)
|
|
63
|
+
export const IconGlobeOutline16 = icon('IconGlobeOutline14', 'IconGlobeOutlineRegular', 14)
|
package/src/client/locales.ts
CHANGED
|
@@ -74,6 +74,13 @@ export const zh = {
|
|
|
74
74
|
'mem.saved': '已保存',
|
|
75
75
|
'mem.edit': '编辑',
|
|
76
76
|
'mem.section-label': '任务记忆',
|
|
77
|
+
// `/` 菜单:自建 slash 源的行文案。分组标题走候选的 section(见 slash.ts),
|
|
78
|
+
// 不走 slash.menu 词典——那个 namespace 由宿主的 ui-input-trigger 独占,重复注册会抛错。
|
|
79
|
+
// 三个行标题直接复用上面面板的 view.* 文案,保证菜单与面板叫同一个名字。
|
|
80
|
+
'slash.group': '工作流',
|
|
81
|
+
'slash.tasks-desc': '任务清单:步骤进度、涉及文件、当前会话绑定',
|
|
82
|
+
'slash.project-desc': '项目记忆:本项目的教训 / 决策 / 流程',
|
|
83
|
+
'slash.global-desc': '全局记忆:跨项目的经验条目',
|
|
77
84
|
}
|
|
78
85
|
|
|
79
86
|
export const en = {
|
|
@@ -149,6 +156,10 @@ export const en = {
|
|
|
149
156
|
'mem.saved': 'Saved',
|
|
150
157
|
'mem.edit': 'Edit',
|
|
151
158
|
'mem.section-label': 'Task memory',
|
|
159
|
+
'slash.group': 'Workflow',
|
|
160
|
+
'slash.tasks-desc': 'Task list: progress, involved files, current session binding',
|
|
161
|
+
'slash.project-desc': 'Project memory: this project’s lessons / decisions / procedures',
|
|
162
|
+
'slash.global-desc': 'Global memory: experience entries shared across projects',
|
|
152
163
|
}
|
|
153
164
|
|
|
154
165
|
export type LocaleDict = typeof zh
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 会话列表快照 → 当前会话 id(宿主版本兼容层)。
|
|
3
|
+
*
|
|
4
|
+
* 快照结构被宿主改过一次,旧写法只认第一种:
|
|
5
|
+
* - dsh ≤0.1.6:`{ current?: id, items: [{ sessionId, blank }] }`
|
|
6
|
+
* - dsh 0.1.7+:`{ ids: id[], byId: Record<id, SessionSummary>, phase, projectionsBySession }`
|
|
7
|
+
* —— `current` 与 `items` **都不存在了**。
|
|
8
|
+
*
|
|
9
|
+
* 后果一(永远 null):0.1.7 上 `snap.current` 和 `snap.items` 都是 undefined → 旧代码永远
|
|
10
|
+
* 返回 null → 面板显示「还没有会话」、记忆视图报 `同步失败: no session / commands service`。
|
|
11
|
+
* 而**任务视图渲染的是 task-data-store 里的缓存快照**,看起来还正常,所以这个故障很容易被
|
|
12
|
+
* 误读成"数据格式/旧版本兼容"问题。
|
|
13
|
+
*
|
|
14
|
+
* 后果二(不跟随切换):光"取一个非 null 的 id"不够 —— 兜底取的是宿主列表里第一个非 blank 的
|
|
15
|
+
* 会话,它**不随用户切换对话而变化**,面板会一直停在同一个会话(从而显示错项目的任务)。
|
|
16
|
+
* 0.1.7 判断"当前会话"的正式依据是 `SessionSummary.retainedBy.mainView > 0`:
|
|
17
|
+
* 主视图正在 retain 的那个就是用户正在看的那个(第一方同款判据,见 ui-layout/DocumentTitle.tsx
|
|
18
|
+
* 与 ui-session)。它就在**列表快照的 byId 行上**,而 retain 计数变化会 `list.set(...)` 重新发布
|
|
19
|
+
* 快照(session-controller/.../sessions/service.ts 的 publishRetention),所以订阅
|
|
20
|
+
* `ctx.sessions.list` 的组件会在切换会话时自动重渲染 —— 这条必须走快照内字段,不能另开
|
|
21
|
+
* `retainInfo()` 订阅,否则切换不会触发重渲染。
|
|
22
|
+
*
|
|
23
|
+
* 实测形状见 `packages/api/session-controller/src/client/sessions/service.ts` 的
|
|
24
|
+
* `SessionListState` / `SessionSummary`。
|
|
25
|
+
*/
|
|
26
|
+
|
|
27
|
+
/** 正数才算持有(retain 计数缺失/0/负数一律当没有)。 */
|
|
28
|
+
function heldCount(value) {
|
|
29
|
+
return typeof value === 'number' && value > 0 ? value : 0
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** 从一行 summary 抽出本模块关心的三个事实。 */
|
|
33
|
+
function rowFacts(row) {
|
|
34
|
+
return {
|
|
35
|
+
blank: row?.blank === true,
|
|
36
|
+
/** 主视图对这条会话的 retain 计数;> 0 即"用户正在看它"。 */
|
|
37
|
+
mainView: heldCount(row?.retainedBy?.mainView),
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* 把两种快照形状统一成 `{ id, blank, mainView }` 列表,顺序保持宿主给的顺序。
|
|
43
|
+
* @param {object|undefined|null} snap - `ctx.sessions.list.getSnapshot()`
|
|
44
|
+
* @returns {Array<{id: string, blank: boolean, mainView: number}>} 认不出的形状返回空数组
|
|
45
|
+
*/
|
|
46
|
+
export function sessionRows(snap) {
|
|
47
|
+
if (!snap || typeof snap !== 'object') return []
|
|
48
|
+
// 旧形状:items 里每行自带 sessionId / blank / retainedBy
|
|
49
|
+
if (Array.isArray(snap.items)) {
|
|
50
|
+
return snap.items
|
|
51
|
+
.map((row) => ({ id: row?.sessionId, ...rowFacts(row) }))
|
|
52
|
+
.filter((row) => typeof row.id === 'string' && row.id !== '')
|
|
53
|
+
}
|
|
54
|
+
const byId = snap.byId && typeof snap.byId === 'object' ? snap.byId : null
|
|
55
|
+
// 0.1.7 形状:ids 表达宿主列表成员(顺序权威),行数据在 byId 里
|
|
56
|
+
if (Array.isArray(snap.ids)) {
|
|
57
|
+
return snap.ids
|
|
58
|
+
.map((id) => ({ id, ...rowFacts(byId?.[id]) }))
|
|
59
|
+
.filter((row) => typeof row.id === 'string' && row.id !== '')
|
|
60
|
+
}
|
|
61
|
+
// 只有 byId 的半成品形状:仍然可用,只是丢了宿主顺序
|
|
62
|
+
if (byId) {
|
|
63
|
+
return Object.keys(byId)
|
|
64
|
+
.map((id) => ({ id, ...rowFacts(byId[id]) }))
|
|
65
|
+
.filter((row) => typeof row.id === 'string' && row.id !== '')
|
|
66
|
+
}
|
|
67
|
+
return []
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* 选出一个可用于执行命令的会话 id(即"用户正在看的那个")。
|
|
72
|
+
*
|
|
73
|
+
* 优先级:
|
|
74
|
+
* 1. 旧形状的显式 `current`(≤0.1.6 的权威字段,保持原语义);
|
|
75
|
+
* 2. `retainedBy.mainView > 0` 的会话(0.1.7 的权威判据,**跟随会话切换**);
|
|
76
|
+
* 3. 第一个非 blank → 第一个(对两种形状都适用的兜底)。
|
|
77
|
+
* @param {object|undefined|null} snap - `ctx.sessions.list.getSnapshot()`
|
|
78
|
+
* @returns {string|null} 会话 id;拿不到返回 null(调用方据此显示「还没有会话」)
|
|
79
|
+
*/
|
|
80
|
+
export function pickSessionId(snap) {
|
|
81
|
+
if (!snap || typeof snap !== 'object') return null
|
|
82
|
+
if (typeof snap.current === 'string' && snap.current !== '') return snap.current
|
|
83
|
+
const rows = sessionRows(snap)
|
|
84
|
+
const live = rows.find((row) => row.mainView > 0)
|
|
85
|
+
if (live) return live.id
|
|
86
|
+
const chosen = rows.find((row) => !row.blank) ?? rows[0]
|
|
87
|
+
return chosen ? chosen.id : null
|
|
88
|
+
}
|