dsh-turn-undo 0.0.5 → 0.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.
- package/client.js +18 -32
- package/index.js +91 -17
- package/package.json +1 -1
package/client.js
CHANGED
|
@@ -146,6 +146,10 @@ window.__ModuleLoader__.load({
|
|
|
146
146
|
'.dtu-trigger{display:inline-flex;align-items:center;justify-content:center;width:24px;height:24px;padding:0;border:0;border-radius:6px;background:transparent;color:var(--dsw-alias-label-tertiary);cursor:pointer}',
|
|
147
147
|
'.dtu-trigger:hover{background:var(--dsw-alias-interactive-bg-hover);color:var(--dsw-alias-label-secondary)}',
|
|
148
148
|
'.dtu-trigger:disabled{cursor:not-allowed;opacity:.5}',
|
|
149
|
+
// 点击后浏览器保留 :focus,hover 底色会「粘住」不恢复 —— 鼠标移开时
|
|
150
|
+
// 显式清掉 focus/active 态的背景与描边。
|
|
151
|
+
'.dtu-trigger:focus{outline:none}',
|
|
152
|
+
'.dtu-trigger:focus:not(:hover),.dtu-trigger:focus-visible:not(:hover),.dtu-trigger:active:not(:hover){background:transparent;color:var(--dsw-alias-label-tertiary)}',
|
|
149
153
|
'.dtu-overlay{position:fixed;inset:0;background:rgba(0,0,0,.5);display:flex;align-items:center;justify-content:center;z-index:10000}',
|
|
150
154
|
'.dtu-dialog{box-sizing:border-box;width:min(560px,100%);max-height:calc(100dvh - 48px);overflow:auto;background:var(--dsw-alias-bg-layer-1);border:1px solid var(--dsw-alias-border-l2);border-radius:12px;box-shadow:0 8px 32px rgba(0,0,0,.2)}',
|
|
151
155
|
'.dtu-body{display:flex;flex-direction:column;gap:14px;width:100%;min-width:0;max-width:100%;box-sizing:border-box;padding:18px}',
|
|
@@ -197,33 +201,7 @@ window.__ModuleLoader__.load({
|
|
|
197
201
|
document.head.appendChild(styleEl)
|
|
198
202
|
}
|
|
199
203
|
|
|
200
|
-
function
|
|
201
|
-
try {
|
|
202
|
-
ctx.sessions.open(sessionId)
|
|
203
|
-
} catch (error) {
|
|
204
|
-
console.warn('[turn-undo] openRestoredSession open failed:', error.message)
|
|
205
|
-
return
|
|
206
|
-
}
|
|
207
|
-
if (!draftText) return
|
|
208
|
-
function trySetDraft(remaining) {
|
|
209
|
-
if (remaining <= 0) return
|
|
210
|
-
try {
|
|
211
|
-
var scope = ctx.sessions.scope(sessionId)
|
|
212
|
-
if (scope !== undefined) {
|
|
213
|
-
ctx.conversation.input.for(scope).setDraft(draftText)
|
|
214
|
-
return
|
|
215
|
-
}
|
|
216
|
-
} catch (error) {
|
|
217
|
-
if (remaining <= 1) {
|
|
218
|
-
console.warn('[turn-undo] openRestoredSession setDraft failed:', error.message)
|
|
219
|
-
}
|
|
220
|
-
}
|
|
221
|
-
setTimeout(function () { trySetDraft(remaining - 1) }, 100)
|
|
222
|
-
}
|
|
223
|
-
trySetDraft(20)
|
|
224
|
-
}
|
|
225
|
-
|
|
226
|
-
ctx.inject(['slots', 'sessions', 'conversation'], function (scope) {
|
|
204
|
+
ctx.inject(['slots', 'conversation', 'uiWorkspace'], function (scope) {
|
|
227
205
|
scope.effect(function () {
|
|
228
206
|
return scope.slots.inject('conversation.session.header.actions', function () {
|
|
229
207
|
return scope.slots.register({
|
|
@@ -231,13 +209,21 @@ window.__ModuleLoader__.load({
|
|
|
231
209
|
id: 'turn-undo-portals',
|
|
232
210
|
order: 100,
|
|
233
211
|
inject: function () {
|
|
234
|
-
var
|
|
212
|
+
var workspaceSvc = scope.uiWorkspace
|
|
235
213
|
var conversationSvc = scope.conversation
|
|
236
214
|
return {
|
|
237
215
|
openRestoredSession: function (newSessionId, draftText) {
|
|
238
|
-
//
|
|
239
|
-
|
|
240
|
-
|
|
216
|
+
// 导航必须走 uiWorkspace:ISessions 没有 open(),
|
|
217
|
+
// 旧的 sessionsSvc.open(...) 永远被 if 守卫静默跳过 —— 这正是
|
|
218
|
+
// 「改名了但新会话没打开」的根因。
|
|
219
|
+
try {
|
|
220
|
+
if (workspaceSvc && typeof workspaceSvc.openSession === 'function') {
|
|
221
|
+
workspaceSvc.openSession(newSessionId)
|
|
222
|
+
} else {
|
|
223
|
+
console.warn('[turn-undo] uiWorkspace.openSession unavailable; new session not opened')
|
|
224
|
+
}
|
|
225
|
+
} catch (e) {
|
|
226
|
+
console.warn('[turn-undo] Failed to open restored session:', e.message)
|
|
241
227
|
}
|
|
242
228
|
if (!conversationSvc || !conversationSvc.input || !draftText) return
|
|
243
229
|
function trySetDraft(remaining) {
|
|
@@ -773,7 +759,7 @@ window.__ModuleLoader__.load({
|
|
|
773
759
|
}
|
|
774
760
|
|
|
775
761
|
exports.apply = apply
|
|
776
|
-
exports.inject = ['slots', '
|
|
762
|
+
exports.inject = ['slots', 'conversation', 'uiWorkspace']
|
|
777
763
|
return module.exports
|
|
778
764
|
},
|
|
779
765
|
})
|
package/index.js
CHANGED
|
@@ -41,8 +41,16 @@ export const name = 'turn-undo'
|
|
|
41
41
|
export { SnapshotStore }
|
|
42
42
|
|
|
43
43
|
// Injected services (via ctx.inject in apply()): webServer, sessions,
|
|
44
|
-
// sessionQuery, agents, sessionController.
|
|
45
|
-
export const inject = [
|
|
44
|
+
// sessionQuery, agents, sessionController, workspaceRegistry.
|
|
45
|
+
export const inject = [
|
|
46
|
+
'webServer',
|
|
47
|
+
'sessions',
|
|
48
|
+
'sessionQuery',
|
|
49
|
+
'agents',
|
|
50
|
+
'sessionTitle',
|
|
51
|
+
'sessionController',
|
|
52
|
+
'workspaceRegistry',
|
|
53
|
+
]
|
|
46
54
|
|
|
47
55
|
// ---- Configuration defaults ----
|
|
48
56
|
const DEFAULT_SNAPSHOT_TTL_DAYS = 7
|
|
@@ -768,11 +776,26 @@ function resolveForkBoundary(source, messageSeq, promptText) {
|
|
|
768
776
|
return { boundary: null, turn: null, cwd, reason: 'no-turn-start' }
|
|
769
777
|
}
|
|
770
778
|
const previousEnd = events.findLast(e => e.type === 'turn/end' && e.seq < start.seq)
|
|
779
|
+
|
|
780
|
+
// 第一条消息(无前一个 turn/end)也必须 fork:边界取 turn/start 前
|
|
781
|
+
// 最近的一个「索引==seq」有效事件(会话初始化完成处),子会话继承
|
|
782
|
+
// workspace/preset/cwd,而不是 create() 一个空白会话 —— 这正是
|
|
783
|
+
// 「点击撤销后直接创建新会话、没有 fork 出会话」的根因。
|
|
784
|
+
let boundary = previousEnd ? previousEnd.seq : null
|
|
785
|
+
if (boundary !== null && !(events[boundary] && events[boundary].seq === boundary)) {
|
|
786
|
+
boundary = null
|
|
787
|
+
}
|
|
788
|
+
if (boundary === null) {
|
|
789
|
+
const startIdx = events.indexOf(start)
|
|
790
|
+
for (let i = startIdx - 1; i >= 0; i--) {
|
|
791
|
+
if (events[i] && events[i].seq === i) { boundary = i; break }
|
|
792
|
+
}
|
|
793
|
+
}
|
|
771
794
|
return {
|
|
772
|
-
boundary
|
|
795
|
+
boundary,
|
|
773
796
|
turn,
|
|
774
797
|
cwd,
|
|
775
|
-
reason:
|
|
798
|
+
reason: boundary !== null ? 'ok' : 'no-previous-end',
|
|
776
799
|
}
|
|
777
800
|
}
|
|
778
801
|
|
|
@@ -1038,20 +1061,55 @@ async function executeSurfaceRewind(agent, targetSeq) {
|
|
|
1038
1061
|
* no completed turn before it (e.g. the very first user message).
|
|
1039
1062
|
* @returns the new child session id.
|
|
1040
1063
|
*/
|
|
1064
|
+
/**
|
|
1065
|
+
* Find the workspace that owns `sessionId`, so a `cwd`-less `create()` can pass
|
|
1066
|
+
* `workspaceId` and get attached to the workspace catalogue (visible in the
|
|
1067
|
+
* sidebar). Mirrors upstream `SessionController.forkWorkspace`: only sessions
|
|
1068
|
+
* already indexed as live are visible through `WorkspaceRegistry.list()`.
|
|
1069
|
+
* @returns the owning workspace id, or `undefined` when the session is not
|
|
1070
|
+
* indexed (caller then falls back to a `cwd`-only create).
|
|
1071
|
+
*/
|
|
1072
|
+
function findWorkspaceIdForSession(ctx, sessionId) {
|
|
1073
|
+
try {
|
|
1074
|
+
const registry = ctx.workspaceRegistry
|
|
1075
|
+
if (!registry || typeof registry.list !== 'function') return undefined
|
|
1076
|
+
for (const workspace of registry.list()) {
|
|
1077
|
+
const ids = workspace && workspace.sessionIds
|
|
1078
|
+
if (Array.isArray(ids) && ids.includes(sessionId)) return workspace.id
|
|
1079
|
+
}
|
|
1080
|
+
} catch (e) {
|
|
1081
|
+
console.warn('[turn-undo] Workspace lookup failed (falling back to cwd):', e.message)
|
|
1082
|
+
}
|
|
1083
|
+
return undefined
|
|
1084
|
+
}
|
|
1085
|
+
|
|
1041
1086
|
async function forkAndMarkUndone(ctx, sessionId, messageSeq, promptText) {
|
|
1042
1087
|
const source = await readSession(ctx, sessionId)
|
|
1043
1088
|
if (!source) throw new Error('source session not found')
|
|
1044
1089
|
const b = resolveForkBoundary(source, messageSeq, promptText)
|
|
1045
1090
|
|
|
1091
|
+
console.info(
|
|
1092
|
+
'[turn-undo] resolveBoundary session=' + sessionId,
|
|
1093
|
+
'messageSeq=' + messageSeq,
|
|
1094
|
+
'boundary=' + String(b.boundary),
|
|
1095
|
+
'turn=' + String(b.turn),
|
|
1096
|
+
'reason=' + b.reason,
|
|
1097
|
+
)
|
|
1046
1098
|
let childId
|
|
1047
1099
|
if (typeof b.boundary === 'number') {
|
|
1100
|
+
// fork() 会自动把子会话 attach 到 source 所在的 workspace(见上游
|
|
1101
|
+
// commands.ts forkWorkspace),所以 fork 分支在侧栏可见。
|
|
1048
1102
|
const { sessionId: cid } = await ctx.sessionController.fork({ sessionId, atSeq: b.boundary })
|
|
1049
1103
|
childId = cid
|
|
1050
1104
|
} else {
|
|
1105
|
+
// create() 只有显式给 workspaceId 才会 attachSession;只给 cwd 的会话
|
|
1106
|
+
// 不会进 workspace 目录,GUI 侧栏看不到 —— 这正是「原会话改名了但没有
|
|
1107
|
+
// 新会话出现」的根因。所以先反查 source 所属 workspace。
|
|
1051
1108
|
const cwd = source.header?.cwd
|
|
1052
1109
|
if (!cwd) throw new Error('cannot undo first message without a cwd')
|
|
1110
|
+
const workspaceId = findWorkspaceIdForSession(ctx, sessionId)
|
|
1053
1111
|
const created = await ctx.sessionController.create({
|
|
1054
|
-
cwd,
|
|
1112
|
+
...(workspaceId !== undefined ? { workspaceId } : { cwd }),
|
|
1055
1113
|
...(source.header?.agentPreset ? { agentPreset: source.header.agentPreset } : {}),
|
|
1056
1114
|
})
|
|
1057
1115
|
childId = created.sessionId
|
|
@@ -1123,7 +1181,20 @@ function createHandler(ctx, runtime, sessions, agents) {
|
|
|
1123
1181
|
})
|
|
1124
1182
|
}
|
|
1125
1183
|
|
|
1126
|
-
|
|
1184
|
+
let preview
|
|
1185
|
+
try {
|
|
1186
|
+
preview = runtime.store.preview(sessionId, targetTurn, getCwd(source))
|
|
1187
|
+
} catch (e) {
|
|
1188
|
+
logger?.warn?.('[turn-undo] preview failed:', e.message)
|
|
1189
|
+
preview = {
|
|
1190
|
+
ok: false,
|
|
1191
|
+
status: 'preview-failed',
|
|
1192
|
+
error: '预览失败:' + e.message,
|
|
1193
|
+
targetTurn,
|
|
1194
|
+
totalChanges: 0,
|
|
1195
|
+
changes: [],
|
|
1196
|
+
}
|
|
1197
|
+
}
|
|
1127
1198
|
return json(response, 200, preview)
|
|
1128
1199
|
}
|
|
1129
1200
|
|
|
@@ -1260,17 +1331,20 @@ export function apply(ctx, config = {}) {
|
|
|
1260
1331
|
})
|
|
1261
1332
|
|
|
1262
1333
|
// HTTP API endpoints.
|
|
1263
|
-
ctx.inject(
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
scope.
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
|
|
1334
|
+
ctx.inject(
|
|
1335
|
+
['webServer', 'sessions', 'sessionQuery', 'agents', 'sessionTitle', 'sessionController', 'workspaceRegistry'],
|
|
1336
|
+
(scope) => {
|
|
1337
|
+
scope.effect(() => {
|
|
1338
|
+
const handler = createHandler(scope, runtime, scope.sessions, scope.agents)
|
|
1339
|
+
scope.webServer.register({
|
|
1340
|
+
kind: 'exact',
|
|
1341
|
+
path: API_PATH,
|
|
1342
|
+
handler,
|
|
1343
|
+
})
|
|
1344
|
+
return () => {}
|
|
1345
|
+
}, 'turn-undo: http-api')
|
|
1346
|
+
},
|
|
1347
|
+
)
|
|
1274
1348
|
}
|
|
1275
1349
|
|
|
1276
1350
|
/**
|