@liziy/session-queue 0.2.0 → 0.2.2
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 +2 -83
- package/index.ts +81 -329
- package/package.json +18 -6
package/README.md
CHANGED
|
@@ -1,84 +1,5 @@
|
|
|
1
1
|
# @liziy/session-queue
|
|
2
2
|
|
|
3
|
-
> **Switch UI language:** `/rollback zh` · `/rollback en` (zh = 中文, en = English)
|
|
4
|
-
|
|
5
|
-
---
|
|
6
|
-
|
|
7
|
-
## English
|
|
8
|
-
|
|
9
|
-
Convert pi's session tree into a linear rollback queue. Every turn auto-records modified files; rollback restores them in one click.
|
|
10
|
-
|
|
11
|
-
### Features
|
|
12
|
-
|
|
13
|
-
- **JIT capture** — snapshots before/after `edit`/`write`/`bash` tool calls (zero startup overhead)
|
|
14
|
-
- **Content-addressed storage** — dedup by sha256
|
|
15
|
-
- **Dirty check** — refuses to overwrite externally modified files
|
|
16
|
-
- **Workspace whitelist** — opt-in per directory; nothing recorded by default
|
|
17
|
-
- **Snapshot GC** — mark-sweep keeps disk usage bounded
|
|
18
|
-
- **bash tracking** — `rm`/`del`/`mv`/`>`/`>>` (multi-file, simple globs)
|
|
19
|
-
- **Session Tree sync** — navigating to an older point auto-rolls back the queue
|
|
20
|
-
- **Zero dependencies** · **i18n: zh / en**
|
|
21
|
-
|
|
22
|
-
### Install
|
|
23
|
-
|
|
24
|
-
```bash
|
|
25
|
-
pi install npm:@liziy/session-queue
|
|
26
|
-
```
|
|
27
|
-
|
|
28
|
-
### Commands
|
|
29
|
-
|
|
30
|
-
| Command | Action |
|
|
31
|
-
|---|---|
|
|
32
|
-
| `/rollback` | Open main menu (rollback / workspace / GC / settings) |
|
|
33
|
-
| `/rollback enable` | Start tracking current directory |
|
|
34
|
-
| `/rollback disable` | Pause tracking |
|
|
35
|
-
| `/rollback list` | List enabled workspaces |
|
|
36
|
-
| `/rollback gc` | Manually run snapshot GC |
|
|
37
|
-
| `/rollback zh` | Switch UI to Chinese |
|
|
38
|
-
| `/rollback en` | Switch UI to English |
|
|
39
|
-
|
|
40
|
-
### Quick Start
|
|
41
|
-
|
|
42
|
-
1. `/rollback enable` — start tracking
|
|
43
|
-
2. Work normally; each `turn_end` writes a checkpoint
|
|
44
|
-
3. `/rollback` → select a checkpoint → confirm → files restored
|
|
45
|
-
4. Optional: open Session Tree, click an older message, type a new question — queue auto-syncs
|
|
46
|
-
|
|
47
|
-
### Status Bar
|
|
48
|
-
|
|
49
|
-
| Indicator | Meaning |
|
|
50
|
-
|---|---|
|
|
51
|
-
| (hidden) | No workspace active |
|
|
52
|
-
| `● 记录` (cyan) | Workspace tracking only |
|
|
53
|
-
| `● 同步` (green) | Tracking + Session Tree auto-sync |
|
|
54
|
-
|
|
55
|
-
### Rollback Semantics
|
|
56
|
-
|
|
57
|
-
- **Files**: each file's `beforeHash` from the **earliest** discarded turn is restored — reverts to the state right before the first modification, not an intermediate snapshot
|
|
58
|
-
- **Queue**: `entries.slice(0, targetIdx)` — drops target and all later entries
|
|
59
|
-
- **Conflicts**: skipped files are kept as a `residual` entry (re-tryable)
|
|
60
|
-
- **Safety**: missing snapshot = skip + warn, never delete blindly
|
|
61
|
-
|
|
62
|
-
### Storage
|
|
63
|
-
|
|
64
|
-
```
|
|
65
|
-
~/.pi/agent/extensions/session-queue/
|
|
66
|
-
├── config.json # workspaces, followSessionTree, lang
|
|
67
|
-
├── queue-{sessionId}.json # last 10 entries + changes
|
|
68
|
-
└── snapshots/{hash}.content # content-addressed
|
|
69
|
-
```
|
|
70
|
-
|
|
71
|
-
### Limits
|
|
72
|
-
|
|
73
|
-
- Tracks `edit` / `write` / partial `bash` only (no `sed -i`, `npm install`, subshells)
|
|
74
|
-
- 10-entry rolling window; older turns are not recorded or restorable
|
|
75
|
-
- Doesn't undo pi's tree; use `/tree` for that
|
|
76
|
-
- Directory create/remove are not reverted
|
|
77
|
-
|
|
78
|
-
---
|
|
79
|
-
|
|
80
|
-
## 中文
|
|
81
|
-
|
|
82
3
|
把 pi 的会话从「树」改为「队列」:每个 turn 自动记录修改的文件,回滚时一键还原。
|
|
83
4
|
|
|
84
5
|
### 特性
|
|
@@ -90,7 +11,7 @@ pi install npm:@liziy/session-queue
|
|
|
90
11
|
- **snapshot GC**:标记-扫描回收,控制磁盘占用
|
|
91
12
|
- **bash 追踪**:`rm`/`del`/`mv`/`>`/`>>`(多文件、简单 glob)
|
|
92
13
|
- **Session Tree 同步**:导航到旧对话点时队列自动回滚
|
|
93
|
-
- **零依赖**
|
|
14
|
+
- **零依赖**
|
|
94
15
|
|
|
95
16
|
### 安装
|
|
96
17
|
|
|
@@ -107,8 +28,6 @@ pi install npm:@liziy/session-queue
|
|
|
107
28
|
| `/rollback disable` | 暂停记录 |
|
|
108
29
|
| `/rollback list` | 列出已启用工作区 |
|
|
109
30
|
| `/rollback gc` | 手动执行 GC |
|
|
110
|
-
| `/rollback zh` | 切换界面为中文 |
|
|
111
|
-
| `/rollback en` | 切换界面为英文 |
|
|
112
31
|
|
|
113
32
|
### 快速上手
|
|
114
33
|
|
|
@@ -136,7 +55,7 @@ pi install npm:@liziy/session-queue
|
|
|
136
55
|
|
|
137
56
|
```
|
|
138
57
|
~/.pi/agent/extensions/session-queue/
|
|
139
|
-
├── config.json # 工作区、followSessionTree
|
|
58
|
+
├── config.json # 工作区、followSessionTree
|
|
140
59
|
├── queue-{sessionId}.json # 最近 10 条 entry + changes
|
|
141
60
|
└── snapshots/{hash}.content # 内容寻址
|
|
142
61
|
```
|
package/index.ts
CHANGED
|
@@ -76,7 +76,6 @@ interface QueueData {
|
|
|
76
76
|
interface Config {
|
|
77
77
|
workspaces: string[];
|
|
78
78
|
followSessionTree?: boolean; // 是否在 Session Tree 导航时自动回滚队列
|
|
79
|
-
lang?: Lang; // 语言:'zh' | 'en',默认 'zh'
|
|
80
79
|
}
|
|
81
80
|
|
|
82
81
|
// =============================================================================
|
|
@@ -115,237 +114,11 @@ const ANSI = {
|
|
|
115
114
|
green: "\x1b[32m", // 亮绿
|
|
116
115
|
};
|
|
117
116
|
|
|
118
|
-
// =============================================================================
|
|
119
|
-
// i18n
|
|
120
|
-
// =============================================================================
|
|
121
|
-
|
|
122
|
-
type Lang = "zh" | "en";
|
|
123
|
-
type I18nValue = string | ((...args: any[]) => string);
|
|
124
|
-
|
|
125
|
-
const I18N: Record<Lang, Record<string, I18nValue>> = {
|
|
126
|
-
zh: {
|
|
127
|
-
// === 状态栏 ===
|
|
128
|
-
"status.sync": `${ANSI.green}●${ANSI.reset} 同步`,
|
|
129
|
-
"status.record": `${ANSI.cyan}●${ANSI.reset} 记录`,
|
|
130
|
-
|
|
131
|
-
// === 主菜单 ===
|
|
132
|
-
"menu.rollbackHistory": (n: number) => `📋 回滚历史 (${n} 个变更点)`,
|
|
133
|
-
"menu.manageWorkspaces": "📂 管理工作区",
|
|
134
|
-
"menu.clearQueue": "🗑️ 清空当前队列",
|
|
135
|
-
"menu.deleteAndClear": "⏸ 删除并清空",
|
|
136
|
-
"menu.enableCurrent": "✅ 启用当前工作区",
|
|
137
|
-
"menu.followSystemTree": (on: boolean) => `🔗 跟随系统Tree (${on ? "开" : "关"})`,
|
|
138
|
-
"menu.collectOrphans": "🧹 回收孤儿快照",
|
|
139
|
-
"menu.exit": "❌ 退出菜单",
|
|
140
|
-
|
|
141
|
-
// === 菜单子项 ===
|
|
142
|
-
"menu.back": "← 返回",
|
|
143
|
-
"menu.workspaceCurrent": " ← 当前",
|
|
144
|
-
"menu.pauseThisWorkspace": "⏸ 暂停此工作区",
|
|
145
|
-
"menu.enableThisWorkspace": "✅ 启用此工作区",
|
|
146
|
-
"menu.removeFromList": "🗑️ 从列表中删除",
|
|
147
|
-
|
|
148
|
-
// === 选择器标题 ===
|
|
149
|
-
"select.mainTitle": "会话队列回滚",
|
|
150
|
-
"select.workspacesTitle": (n: number) => `已启用的工作区 (${n})`,
|
|
151
|
-
"select.workspaceActionTitle": (ws: string) => `工作区: ${ws}`,
|
|
152
|
-
"select.rollbackTitle": (n: number) => `回滚到哪个检查点?(${n} 个检查点 + 当前状态)`,
|
|
153
|
-
|
|
154
|
-
// === 确认弹窗 ===
|
|
155
|
-
"confirm.rollbackTitle": (text: string) => `确认回滚到「${text}」?`,
|
|
156
|
-
"confirm.willProcess": (n: number) => `将处理 ${n} 个文件变更:`,
|
|
157
|
-
"confirm.conflictsWarn": (n: number) => `⚠ ${n} 个文件被外部修改:`,
|
|
158
|
-
"confirm.btnForceOverwrite": "✅ 强制覆盖并回滚",
|
|
159
|
-
"confirm.btnSkipConflicts": "⏭ 跳过冲突,回滚其他",
|
|
160
|
-
"confirm.btnConfirm": "✅ 确定回滚",
|
|
161
|
-
"confirm.btnCancel": "❌ 取消",
|
|
162
|
-
|
|
163
|
-
// === 预览格式 ===
|
|
164
|
-
"format.restore": (n: number, files: string) => ` 还原(${n}):${files}`,
|
|
165
|
-
"format.create": (n: number, files: string) => ` 新增(${n}):${files}`,
|
|
166
|
-
"format.remove": (n: number, files: string) => ` 删除(${n}):${files}`,
|
|
167
|
-
"format.noFileChanges": " (无文件变更)",
|
|
168
|
-
"format.bashTag": " ⚠bash",
|
|
169
|
-
"format.currentNode": " ← 当前节点",
|
|
170
|
-
"format.currentState": "当前状态",
|
|
171
|
-
"format.restored": (n: number) => `还原 ${n}`,
|
|
172
|
-
"format.created": (n: number) => `新增 ${n}`,
|
|
173
|
-
"format.removed": (n: number) => `删除 ${n}`,
|
|
174
|
-
"format.skippedConflicts": (n: number) => `,跳过 ${n} 个冲突`,
|
|
175
|
-
"format.skipped": (n: number) => `,跳过 ${n} 个`,
|
|
176
|
-
"format.missingSnapshot": (n: number) => `(其中 ${n} 个快照丢失,请手动检查)`,
|
|
177
|
-
"format.noChange": "无变更",
|
|
178
|
-
|
|
179
|
-
// === 通知 ===
|
|
180
|
-
"notify.needInteractive": "rollback 需要交互模式",
|
|
181
|
-
"notify.noSession": "无活跃 session",
|
|
182
|
-
"notify.processing": "正在处理上一轮,请稍候",
|
|
183
|
-
"notify.emptyQueue": "队列为空",
|
|
184
|
-
"notify.onlyTurn0": "只有 Turn 0,没有可回滚的记录",
|
|
185
|
-
"notify.noCheckpoints": "没有可回滚的检查点",
|
|
186
|
-
"notify.currentNode": "当前节点不可回滚,请选其他 turn",
|
|
187
|
-
"notify.rolledBack": (text: string) => `已回滚到「${text}」`,
|
|
188
|
-
"notify.rolledBackWith": (text: string, parts: string) => `已回滚到「${text}」:${parts}`,
|
|
189
|
-
"notify.autoRollback": (text: string) => `↩️ Session Tree 导航 → 自动回滚到「${text}」`,
|
|
190
|
-
"notify.autoRollbackWith": (text: string, parts: string) => `↩️ Session Tree 导航 → 自动回滚到「${text}」:${parts}`,
|
|
191
|
-
"notify.queueCleared": (snap: number, queue?: number) => `✅ 队列已清空,回收 ${snap} 个孤儿快照${queue ? `,清理 ${queue} 个旧 queue 文件` : ""}`,
|
|
192
|
-
"notify.deletedAndCleared": (snap: number, queue?: number) => `⏸ 已删除并清空当前 session 的记录,回收 ${snap} 个孤儿快照${queue ? `,清理 ${queue} 个旧 queue 文件` : ""}`,
|
|
193
|
-
"notify.paused": (ws: string, snap: number, queue?: number) => `⏸ 已暂停:${ws},回收 ${snap} 个孤儿快照${queue ? `,清理 ${queue} 个旧 queue 文件` : ""}`,
|
|
194
|
-
"notify.removedFromList": (ws: string) => `🗑️ 已从列表中删除:${ws}`,
|
|
195
|
-
"notify.enabledTracking": (path: string) => `✅ 已启用变更记录:${path}`,
|
|
196
|
-
"notify.enabled": (ws: string) => `✅ 已启用:${ws}`,
|
|
197
|
-
"notify.noWorkspace": "当前工作区未启用",
|
|
198
|
-
"notify.workspaceActive": "当前工作区已启用",
|
|
199
|
-
"notify.otherActive": "已有其他工作区启用中,请先暂停",
|
|
200
|
-
"notify.noWorkspaces": "没有已启用的工作区",
|
|
201
|
-
"notify.queueAlreadyEmpty": "队列已经为空",
|
|
202
|
-
"notify.followOn": "🔗 已开启:Session Tree 导航将自动回滚队列",
|
|
203
|
-
"notify.followOff": "⏸ 已关闭:Session Tree 导航不会自动回滚队列",
|
|
204
|
-
"notify.langSwitched": (lang: string) => `🌐 已切换语言:${lang}(zh=中文,en=English)`,
|
|
205
|
-
"notify.langInvalid": (lang: string) => `⚠ 无效语言代码:${lang}(可选:zh、en)`,
|
|
206
|
-
"notify.gcResult": (scanned: number, deleted: number, live: number, queueDeleted?: number, queueKept?: number) =>
|
|
207
|
-
`🧹 扫描 ${scanned} 个快照,删除 ${deleted} 个孤儿,存活 ${live} 个` +
|
|
208
|
-
`${queueDeleted ? `;清理 ${queueDeleted} 个旧 queue 文件,保留 ${queueKept ?? 0} 个` : ""}`,
|
|
209
|
-
|
|
210
|
-
// === Debug ===
|
|
211
|
-
"debug.fromExtension": "[SQ-debug] session_tree: fromExtension=true 跳过",
|
|
212
|
-
"debug.activeWorkspaceNull": (sess: string) => `[SQ-debug] session_tree: activeWorkspace=null, currentSession=${sess}`,
|
|
213
|
-
"debug.toggleOff": "[SQ-debug] session_tree: toggle off",
|
|
214
|
-
"debug.isProcessing": "[SQ-debug] session_tree: isProcessing=true",
|
|
215
|
-
"debug.noNav": (id: string) => `[SQ-debug] session_tree: 无实际导航 (newLeafId===oldLeafId=${id})`,
|
|
216
|
-
"debug.emptyQueue": "[SQ-debug] session_tree: queue.entries=[]",
|
|
217
|
-
"debug.nav": (src: string, navId: string, leafId: string, qLen: number, qCi: number, sess: string) =>
|
|
218
|
-
`[SQ-debug] nav: ${src}=${navId}, leafId=${leafId}, queue(${qLen}e/${qCi}ci), session=${sess}`,
|
|
219
|
-
"debug.noNavMsg": "[SQ-debug] session_tree: 未找到 nav user msg",
|
|
220
|
-
"debug.navNoMatch": (navId: string, entries: string) => `[SQ-debug] session_tree: nav=${navId} 未匹配 entries=${entries}`,
|
|
221
|
-
"debug.idxOutOfRange": (idx: number, len: number) => `[SQ-debug] session_tree: idx=${idx} >= ${len}(超出队列范围),跳过`,
|
|
222
|
-
"debug.rollbackFired": (idx: number, len: number) => `[SQ-debug] session_tree 回滚 idx=${idx} entries=${len}`,
|
|
223
|
-
"debug.exception": (msg: string) => `[SQ-debug] session_tree 异常: ${msg}`,
|
|
224
|
-
|
|
225
|
-
// === 命令描述 ===
|
|
226
|
-
"cmd.description": "会话队列回滚(单入口菜单);/rollback zh|en 切换语言",
|
|
227
|
-
},
|
|
228
|
-
|
|
229
|
-
en: {
|
|
230
|
-
// === Status bar ===
|
|
231
|
-
"status.sync": `${ANSI.green}●${ANSI.reset} Sync`,
|
|
232
|
-
"status.record": `${ANSI.cyan}●${ANSI.reset} Record`,
|
|
233
|
-
|
|
234
|
-
// === Main menu ===
|
|
235
|
-
"menu.rollbackHistory": (n: number) => `📋 Rollback History (${n} checkpoints)`,
|
|
236
|
-
"menu.manageWorkspaces": "📂 Manage Workspaces",
|
|
237
|
-
"menu.clearQueue": "🗑️ Clear Current Queue",
|
|
238
|
-
"menu.deleteAndClear": "⏸ Delete & Clear",
|
|
239
|
-
"menu.enableCurrent": "✅ Enable Current Workspace",
|
|
240
|
-
"menu.followSystemTree": (on: boolean) => `🔗 Follow System Tree (${on ? "on" : "off"})`,
|
|
241
|
-
"menu.collectOrphans": "🧹 Collect Orphan Snapshots",
|
|
242
|
-
"menu.exit": "❌ Exit Menu",
|
|
243
|
-
|
|
244
|
-
// === Submenu ===
|
|
245
|
-
"menu.back": "← Back",
|
|
246
|
-
"menu.workspaceCurrent": " ← current",
|
|
247
|
-
"menu.pauseThisWorkspace": "⏸ Pause This Workspace",
|
|
248
|
-
"menu.enableThisWorkspace": "✅ Enable This Workspace",
|
|
249
|
-
"menu.removeFromList": "🗑️ Remove From List",
|
|
250
|
-
|
|
251
|
-
// === Select titles ===
|
|
252
|
-
"select.mainTitle": "Session Queue Rollback",
|
|
253
|
-
"select.workspacesTitle": (n: number) => `Enabled Workspaces (${n})`,
|
|
254
|
-
"select.workspaceActionTitle": (ws: string) => `Workspace: ${ws}`,
|
|
255
|
-
"select.rollbackTitle": (n: number) => `Rollback to which checkpoint? (${n} checkpoints + current state)`,
|
|
256
|
-
|
|
257
|
-
// === Confirm dialog ===
|
|
258
|
-
"confirm.rollbackTitle": (text: string) => `Confirm rollback to "${text}"?`,
|
|
259
|
-
"confirm.willProcess": (n: number) => `Will process ${n} file change(s):`,
|
|
260
|
-
"confirm.conflictsWarn": (n: number) => `⚠ ${n} file(s) externally modified:`,
|
|
261
|
-
"confirm.btnForceOverwrite": "✅ Force overwrite and rollback",
|
|
262
|
-
"confirm.btnSkipConflicts": "⏭ Skip conflicts, rollback others",
|
|
263
|
-
"confirm.btnConfirm": "✅ Confirm Rollback",
|
|
264
|
-
"confirm.btnCancel": "❌ Cancel",
|
|
265
|
-
|
|
266
|
-
// === Preview format ===
|
|
267
|
-
"format.restore": (n: number, files: string) => ` Restore (${n}): ${files}`,
|
|
268
|
-
"format.create": (n: number, files: string) => ` Create (${n}): ${files}`,
|
|
269
|
-
"format.remove": (n: number, files: string) => ` Remove (${n}): ${files}`,
|
|
270
|
-
"format.noFileChanges": " (no file changes)",
|
|
271
|
-
"format.bashTag": " ⚠bash",
|
|
272
|
-
"format.currentNode": " ← current node",
|
|
273
|
-
"format.currentState": "Current State",
|
|
274
|
-
"format.restored": (n: number) => `Restored ${n}`,
|
|
275
|
-
"format.created": (n: number) => `Created ${n}`,
|
|
276
|
-
"format.removed": (n: number) => `Removed ${n}`,
|
|
277
|
-
"format.skippedConflicts": (n: number) => `, skipped ${n} conflict(s)`,
|
|
278
|
-
"format.skipped": (n: number) => `, skipped ${n}`,
|
|
279
|
-
"format.missingSnapshot": (n: number) => `(${n} snapshot(s) missing, please check manually)`,
|
|
280
|
-
"format.noChange": "no changes",
|
|
281
|
-
|
|
282
|
-
// === Notifications ===
|
|
283
|
-
"notify.needInteractive": "rollback requires interactive mode",
|
|
284
|
-
"notify.noSession": "No active session",
|
|
285
|
-
"notify.processing": "Processing previous turn, please wait",
|
|
286
|
-
"notify.emptyQueue": "Queue is empty",
|
|
287
|
-
"notify.onlyTurn0": "Only Turn 0, no rollback records",
|
|
288
|
-
"notify.noCheckpoints": "No checkpoints to roll back to",
|
|
289
|
-
"notify.currentNode": "Current node cannot be rolled back, choose another turn",
|
|
290
|
-
"notify.rolledBack": (text: string) => `Rolled back to "${text}"`,
|
|
291
|
-
"notify.rolledBackWith": (text: string, parts: string) => `Rolled back to "${text}": ${parts}`,
|
|
292
|
-
"notify.autoRollback": (text: string) => `↩️ Session Tree nav → auto rollback to "${text}"`,
|
|
293
|
-
"notify.autoRollbackWith": (text: string, parts: string) => `↩️ Session Tree nav → auto rollback to "${text}": ${parts}`,
|
|
294
|
-
"notify.queueCleared": (snap: number, queue?: number) => `✅ Queue cleared, collected ${snap} orphan snapshot(s)${queue ? `, cleaned ${queue} old queue file(s)` : ""}`,
|
|
295
|
-
"notify.deletedAndCleared": (snap: number, queue?: number) => `⏸ Deleted and cleared current session's records, collected ${snap} orphan snapshot(s)${queue ? `, cleaned ${queue} old queue file(s)` : ""}`,
|
|
296
|
-
"notify.paused": (ws: string, snap: number, queue?: number) => `⏸ Paused: ${ws}, collected ${snap} orphan snapshot(s)${queue ? `, cleaned ${queue} old queue file(s)` : ""}`,
|
|
297
|
-
"notify.removedFromList": (ws: string) => `🗑️ Removed from list: ${ws}`,
|
|
298
|
-
"notify.enabledTracking": (path: string) => `✅ Tracking enabled: ${path}`,
|
|
299
|
-
"notify.enabled": (ws: string) => `✅ Enabled: ${ws}`,
|
|
300
|
-
"notify.noWorkspace": "Current workspace not enabled",
|
|
301
|
-
"notify.workspaceActive": "Current workspace already enabled",
|
|
302
|
-
"notify.otherActive": "Another workspace is active, please pause it first",
|
|
303
|
-
"notify.noWorkspaces": "No enabled workspaces",
|
|
304
|
-
"notify.queueAlreadyEmpty": "Queue is already empty",
|
|
305
|
-
"notify.followOn": "🔗 Enabled: Session Tree navigation will auto-rollback queue",
|
|
306
|
-
"notify.followOff": "⏸ Disabled: Session Tree navigation will not auto-rollback queue",
|
|
307
|
-
"notify.langSwitched": (lang: string) => `🌐 Language switched: ${lang} (zh=中文, en=English)`,
|
|
308
|
-
"notify.langInvalid": (lang: string) => `⚠ Invalid language code: ${lang} (options: zh, en)`,
|
|
309
|
-
"notify.gcResult": (scanned: number, deleted: number, live: number, queueDeleted?: number, queueKept?: number) =>
|
|
310
|
-
`🧹 Scanned ${scanned} snapshot(s), deleted ${deleted} orphan(s), ${live} live` +
|
|
311
|
-
`${queueDeleted ? `; cleaned ${queueDeleted} old queue file(s), kept ${queueKept ?? 0}` : ""}`,
|
|
312
|
-
|
|
313
|
-
// === Debug ===
|
|
314
|
-
"debug.fromExtension": "[SQ-debug] session_tree: fromExtension=true skipped",
|
|
315
|
-
"debug.activeWorkspaceNull": (sess: string) => `[SQ-debug] session_tree: activeWorkspace=null, currentSession=${sess}`,
|
|
316
|
-
"debug.toggleOff": "[SQ-debug] session_tree: toggle off",
|
|
317
|
-
"debug.isProcessing": "[SQ-debug] session_tree: isProcessing=true",
|
|
318
|
-
"debug.noNav": (id: string) => `[SQ-debug] session_tree: no actual nav (newLeafId===oldLeafId=${id})`,
|
|
319
|
-
"debug.emptyQueue": "[SQ-debug] session_tree: queue.entries=[]",
|
|
320
|
-
"debug.nav": (src: string, navId: string, leafId: string, qLen: number, qCi: number, sess: string) =>
|
|
321
|
-
`[SQ-debug] nav: ${src}=${navId}, leafId=${leafId}, queue(${qLen}e/${qCi}ci), session=${sess}`,
|
|
322
|
-
"debug.noNavMsg": "[SQ-debug] session_tree: no nav user msg found",
|
|
323
|
-
"debug.navNoMatch": (navId: string, entries: string) => `[SQ-debug] session_tree: nav=${navId} not matched entries=${entries}`,
|
|
324
|
-
"debug.idxOutOfRange": (idx: number, len: number) => `[SQ-debug] session_tree: idx=${idx} >= ${len} (out of range), skipped`,
|
|
325
|
-
"debug.rollbackFired": (idx: number, len: number) => `[SQ-debug] session_tree rollback idx=${idx} entries=${len}`,
|
|
326
|
-
"debug.exception": (msg: string) => `[SQ-debug] session_tree exception: ${msg}`,
|
|
327
|
-
|
|
328
|
-
// === Command description ===
|
|
329
|
-
"cmd.description": "Session queue rollback (single-entry menu); /rollback zh|en to switch language",
|
|
330
|
-
},
|
|
331
|
-
};
|
|
332
|
-
|
|
333
|
-
let currentLang: Lang = "zh";
|
|
334
|
-
|
|
335
|
-
function setLang(lang: Lang): void {
|
|
336
|
-
if (I18N[lang]) currentLang = lang;
|
|
337
|
-
}
|
|
338
|
-
|
|
339
|
-
function t(key: string, ...args: any[]): string {
|
|
340
|
-
const entry = I18N[currentLang]?.[key] ?? I18N.zh[key] ?? key;
|
|
341
|
-
return typeof entry === "function" ? entry(...args) : entry;
|
|
342
|
-
}
|
|
343
|
-
|
|
344
117
|
// 状态文本(3 态:未启用 / 仅记录 / 记录+同步)
|
|
345
118
|
function getStatusText(): string | undefined {
|
|
346
119
|
if (!activeWorkspace) return undefined; // 状态 1:未启用 → 不显示
|
|
347
|
-
if (followSessionTreeEnabled) return
|
|
348
|
-
return
|
|
120
|
+
if (followSessionTreeEnabled) return `${ANSI.green}●${ANSI.reset} 同步`; // 状态 3:亮绿点
|
|
121
|
+
return `${ANSI.cyan}●${ANSI.reset} 记录`; // 状态 2:青色点
|
|
349
122
|
}
|
|
350
123
|
|
|
351
124
|
// =============================================================================
|
|
@@ -485,9 +258,6 @@ function loadConfig(): Config {
|
|
|
485
258
|
if (typeof cfg.followSessionTree === "boolean") {
|
|
486
259
|
followSessionTreeEnabled = cfg.followSessionTree;
|
|
487
260
|
}
|
|
488
|
-
if (cfg.lang) {
|
|
489
|
-
setLang(cfg.lang);
|
|
490
|
-
}
|
|
491
261
|
return cfg;
|
|
492
262
|
} catch {
|
|
493
263
|
return { workspaces: [] };
|
|
@@ -1148,15 +918,15 @@ function previewRollback(plan: RollbackPlan, workspace: string): RollbackPreview
|
|
|
1148
918
|
function formatPreview(preview: RollbackPreview): string {
|
|
1149
919
|
const lines: string[] = [];
|
|
1150
920
|
if (preview.restore.length > 0) {
|
|
1151
|
-
lines.push(
|
|
921
|
+
lines.push(` 还原(${preview.restore.length}):${preview.restore.join(", ")}`);
|
|
1152
922
|
}
|
|
1153
923
|
if (preview.create.length > 0) {
|
|
1154
|
-
lines.push(
|
|
924
|
+
lines.push(` 新增(${preview.create.length}):${preview.create.join(", ")}`);
|
|
1155
925
|
}
|
|
1156
926
|
if (preview.remove.length > 0) {
|
|
1157
|
-
lines.push(
|
|
927
|
+
lines.push(` 删除(${preview.remove.length}):${preview.remove.join(", ")}`);
|
|
1158
928
|
}
|
|
1159
|
-
if (lines.length === 0) lines.push(
|
|
929
|
+
if (lines.length === 0) lines.push(" (无文件变更)");
|
|
1160
930
|
return lines.join("\n");
|
|
1161
931
|
}
|
|
1162
932
|
|
|
@@ -1256,10 +1026,10 @@ function autoRollbackForSessionTree(
|
|
|
1256
1026
|
|
|
1257
1027
|
// 通知
|
|
1258
1028
|
const parts: string[] = [];
|
|
1259
|
-
if (result.restored > 0) parts.push(
|
|
1260
|
-
let msg =
|
|
1029
|
+
if (result.restored > 0) parts.push(`还原 ${result.restored}`);
|
|
1030
|
+
let msg = `↩️ Session Tree 导航 → 自动回滚到「${targetEntry.text.slice(0, 30)}」`;
|
|
1261
1031
|
if (parts.length > 0) msg += `:${parts.join("、")}`;
|
|
1262
|
-
if (result.skipped.length > 0) msg +=
|
|
1032
|
+
if (result.skipped.length > 0) msg += `,跳过 ${result.skipped.length} 个冲突`;
|
|
1263
1033
|
ctx.ui.notify(msg, result.skipped.length > 0 ? "warning" : "info");
|
|
1264
1034
|
}
|
|
1265
1035
|
|
|
@@ -1280,25 +1050,25 @@ function buildQueueItems(queue: QueueData): QueueItem[] {
|
|
|
1280
1050
|
|
|
1281
1051
|
async function showRollbackUI(pi: ExtensionAPI, ctx: any): Promise<void> {
|
|
1282
1052
|
if (!ctx.hasUI) {
|
|
1283
|
-
ctx.ui.notify(
|
|
1053
|
+
ctx.ui.notify("rollback 需要交互模式", "error");
|
|
1284
1054
|
return;
|
|
1285
1055
|
}
|
|
1286
1056
|
if (!currentSessionId) {
|
|
1287
|
-
ctx.ui.notify(
|
|
1057
|
+
ctx.ui.notify("无活跃 session", "error");
|
|
1288
1058
|
return;
|
|
1289
1059
|
}
|
|
1290
1060
|
if (isProcessing) {
|
|
1291
|
-
ctx.ui.notify(
|
|
1061
|
+
ctx.ui.notify("正在处理上一轮,请稍候", "warning");
|
|
1292
1062
|
return;
|
|
1293
1063
|
}
|
|
1294
1064
|
|
|
1295
1065
|
const queue = loadQueue(currentSessionId);
|
|
1296
1066
|
if (queue.entries.length === 0) {
|
|
1297
|
-
ctx.ui.notify(
|
|
1067
|
+
ctx.ui.notify("队列为空", "info");
|
|
1298
1068
|
return;
|
|
1299
1069
|
}
|
|
1300
1070
|
if (queue.entries.length === 1) {
|
|
1301
|
-
ctx.ui.notify(
|
|
1071
|
+
ctx.ui.notify("只有 Turn 0,没有可回滚的记录", "info");
|
|
1302
1072
|
return;
|
|
1303
1073
|
}
|
|
1304
1074
|
|
|
@@ -1310,7 +1080,7 @@ async function showRollbackUI(pi: ExtensionAPI, ctx: any): Promise<void> {
|
|
|
1310
1080
|
const phantomCurrent: QueueItem = {
|
|
1311
1081
|
entry: {
|
|
1312
1082
|
turnIndex: queue.entries.length,
|
|
1313
|
-
text:
|
|
1083
|
+
text: "当前状态",
|
|
1314
1084
|
timestamp: new Date().toISOString(),
|
|
1315
1085
|
changes: [],
|
|
1316
1086
|
},
|
|
@@ -1320,17 +1090,17 @@ async function showRollbackUI(pi: ExtensionAPI, ctx: any): Promise<void> {
|
|
|
1320
1090
|
(it) => !it.entry.residual && !(it.entry.turnIndex === 0 && it.entry.text === "(会话起点)"),
|
|
1321
1091
|
);
|
|
1322
1092
|
if (checkpoints.length === 0) {
|
|
1323
|
-
ctx.ui.notify(
|
|
1093
|
+
ctx.ui.notify("没有可回滚的检查点", "info");
|
|
1324
1094
|
return;
|
|
1325
1095
|
}
|
|
1326
1096
|
const orderedDisplay: QueueItem[] = [phantomCurrent, ...[...checkpoints].reverse()];
|
|
1327
1097
|
|
|
1328
1098
|
const selectOptions = orderedDisplay.map((item) => {
|
|
1329
|
-
const bashTag = item.entry.changes.some((c) => c.viaBash) ?
|
|
1330
|
-
const curTag = item.isCurrent ?
|
|
1099
|
+
const bashTag = item.entry.changes.some((c) => c.viaBash) ? " ⚠bash" : "";
|
|
1100
|
+
const curTag = item.isCurrent ? " ← 当前节点" : "";
|
|
1331
1101
|
let label: string;
|
|
1332
1102
|
if (item.isCurrent) {
|
|
1333
|
-
label =
|
|
1103
|
+
label = "当前状态";
|
|
1334
1104
|
} else {
|
|
1335
1105
|
label = item.entry.text;
|
|
1336
1106
|
}
|
|
@@ -1341,7 +1111,7 @@ async function showRollbackUI(pi: ExtensionAPI, ctx: any): Promise<void> {
|
|
|
1341
1111
|
let targetEntry: QueueEntry | null = null;
|
|
1342
1112
|
while (targetEntry === null) {
|
|
1343
1113
|
const choice = await ctx.ui.select(
|
|
1344
|
-
|
|
1114
|
+
`回滚到哪个检查点?(${checkpoints.length} 个检查点 + 当前状态)`,
|
|
1345
1115
|
selectOptions,
|
|
1346
1116
|
);
|
|
1347
1117
|
if (!choice) return; // 用户取消
|
|
@@ -1349,7 +1119,7 @@ async function showRollbackUI(pi: ExtensionAPI, ctx: any): Promise<void> {
|
|
|
1349
1119
|
if (choiceIdx < 0) return;
|
|
1350
1120
|
const picked = orderedDisplay[choiceIdx];
|
|
1351
1121
|
if (picked.isCurrent) {
|
|
1352
|
-
ctx.ui.notify(
|
|
1122
|
+
ctx.ui.notify("当前节点不可回滚,请选其他 turn", "info");
|
|
1353
1123
|
continue; // 重新提示
|
|
1354
1124
|
}
|
|
1355
1125
|
targetEntry = picked.entry;
|
|
@@ -1370,15 +1140,15 @@ async function showRollbackUI(pi: ExtensionAPI, ctx: any): Promise<void> {
|
|
|
1370
1140
|
saveQueue(queue);
|
|
1371
1141
|
maybeGc();
|
|
1372
1142
|
lastFlushedUserMsgId = null;
|
|
1373
|
-
ctx.ui.notify(
|
|
1143
|
+
ctx.ui.notify(`已回滚到「${targetEntry.text.slice(0, 30)}」`, "info");
|
|
1374
1144
|
return;
|
|
1375
1145
|
}
|
|
1376
1146
|
|
|
1377
1147
|
// ── 第三步:单次确认弹窗(确定/取消 + 冲突处理一起) ──
|
|
1378
1148
|
const preview = previewRollback(plan, activeWorkspace || process.cwd());
|
|
1379
1149
|
let confirmTitle =
|
|
1380
|
-
|
|
1381
|
-
|
|
1150
|
+
`确认回滚到「${targetEntry.text.slice(0, 30)}」?\n` +
|
|
1151
|
+
`将处理 ${plan.changes.length} 个文件变更:\n` +
|
|
1382
1152
|
formatPreview(preview);
|
|
1383
1153
|
const hasConflict = plan.conflictPaths.length > 0;
|
|
1384
1154
|
let confirmButtons: string[];
|
|
@@ -1386,14 +1156,14 @@ async function showRollbackUI(pi: ExtensionAPI, ctx: any): Promise<void> {
|
|
|
1386
1156
|
const conflictList = plan.conflictPaths
|
|
1387
1157
|
.map((p) => " ⚠ " + path.relative(activeWorkspace || process.cwd(), p))
|
|
1388
1158
|
.join("\n");
|
|
1389
|
-
confirmTitle +=
|
|
1159
|
+
confirmTitle += `\n⚠ ${plan.conflictPaths.length} 个文件被外部修改:\n` + conflictList;
|
|
1390
1160
|
confirmButtons = [
|
|
1391
|
-
|
|
1392
|
-
|
|
1393
|
-
|
|
1161
|
+
"✅ 强制覆盖并回滚",
|
|
1162
|
+
"⏭ 跳过冲突,回滚其他",
|
|
1163
|
+
"❌ 取消",
|
|
1394
1164
|
];
|
|
1395
1165
|
} else {
|
|
1396
|
-
confirmButtons = [
|
|
1166
|
+
confirmButtons = ["✅ 确定回滚", "❌ 取消"];
|
|
1397
1167
|
}
|
|
1398
1168
|
|
|
1399
1169
|
const confirm = await ctx.ui.select(confirmTitle, confirmButtons);
|
|
@@ -1448,15 +1218,15 @@ async function showRollbackUI(pi: ExtensionAPI, ctx: any): Promise<void> {
|
|
|
1448
1218
|
maybeGc(true);
|
|
1449
1219
|
|
|
1450
1220
|
const parts: string[] = [];
|
|
1451
|
-
if (preview.restore.length > 0) parts.push(
|
|
1452
|
-
if (preview.create.length > 0) parts.push(
|
|
1453
|
-
if (preview.remove.length > 0) parts.push(
|
|
1454
|
-
let msg =
|
|
1221
|
+
if (preview.restore.length > 0) parts.push(`还原 ${preview.restore.length}`);
|
|
1222
|
+
if (preview.create.length > 0) parts.push(`新增 ${preview.create.length}`);
|
|
1223
|
+
if (preview.remove.length > 0) parts.push(`删除 ${preview.remove.length}`);
|
|
1224
|
+
let msg = `已回滚到「${targetEntry.text.slice(0, 30)}」:${parts.join("、") || "无变更"}`;
|
|
1455
1225
|
if (result.skipped.length > 0) {
|
|
1456
|
-
msg +=
|
|
1226
|
+
msg += `,跳过 ${result.skipped.length} 个`;
|
|
1457
1227
|
}
|
|
1458
1228
|
if (result.missingSnapshot.length > 0) {
|
|
1459
|
-
msg +=
|
|
1229
|
+
msg += `(其中 ${result.missingSnapshot.length} 个快照丢失,请手动检查)`;
|
|
1460
1230
|
}
|
|
1461
1231
|
ctx.ui.notify(msg, result.missingSnapshot.length > 0 ? "warning" : "success");
|
|
1462
1232
|
} finally {
|
|
@@ -1501,30 +1271,30 @@ export default function (pi: ExtensionAPI) {
|
|
|
1501
1271
|
// 改为:直接拿 branch 中最后一条 user 消息 id 作为 sessionEntryId 匹配。
|
|
1502
1272
|
pi.on("session_tree", (event, ctx) => {
|
|
1503
1273
|
if (event.fromExtension) {
|
|
1504
|
-
ctx.ui.notify(
|
|
1274
|
+
ctx.ui.notify("[SQ-debug] session_tree: fromExtension=true 跳过", "info");
|
|
1505
1275
|
return;
|
|
1506
1276
|
}
|
|
1507
1277
|
if (!activeWorkspace) {
|
|
1508
|
-
ctx.ui.notify(
|
|
1278
|
+
ctx.ui.notify(`[SQ-debug] session_tree: activeWorkspace=null, currentSession=${currentSessionId?.slice(-12) ?? "?"}`, "info");
|
|
1509
1279
|
return;
|
|
1510
1280
|
}
|
|
1511
1281
|
if (!ctx.hasUI) return;
|
|
1512
1282
|
if (!followSessionTreeEnabled) {
|
|
1513
|
-
ctx.ui.notify(
|
|
1283
|
+
ctx.ui.notify("[SQ-debug] session_tree: toggle off", "info");
|
|
1514
1284
|
return;
|
|
1515
1285
|
}
|
|
1516
1286
|
if (isProcessing) {
|
|
1517
|
-
ctx.ui.notify(
|
|
1287
|
+
ctx.ui.notify("[SQ-debug] session_tree: isProcessing=true", "info");
|
|
1518
1288
|
return;
|
|
1519
1289
|
}
|
|
1520
1290
|
if (event.newLeafId === event.oldLeafId) {
|
|
1521
|
-
ctx.ui.notify(
|
|
1291
|
+
ctx.ui.notify(`[SQ-debug] session_tree: 无实际导航 (newLeafId===oldLeafId=${event.newLeafId?.slice(0, 8) ?? "?"})`, "info");
|
|
1522
1292
|
return;
|
|
1523
1293
|
}
|
|
1524
1294
|
try {
|
|
1525
1295
|
const queue = loadQueue(currentSessionId || "");
|
|
1526
1296
|
if (queue.entries.length === 0) {
|
|
1527
|
-
ctx.ui.notify(
|
|
1297
|
+
ctx.ui.notify("[SQ-debug] session_tree: queue.entries=[]", "info");
|
|
1528
1298
|
return;
|
|
1529
1299
|
}
|
|
1530
1300
|
|
|
@@ -1574,11 +1344,11 @@ export default function (pi: ExtensionAPI) {
|
|
|
1574
1344
|
}
|
|
1575
1345
|
|
|
1576
1346
|
ctx.ui.notify(
|
|
1577
|
-
|
|
1347
|
+
`[SQ-debug] nav: ${navSource}=${navUserMsgId?.slice(0, 8) ?? "?"}, leafId=${leafId?.slice(0, 8) ?? "?"}, queue(${queue.entries.length}e/${queue.currentIndex}ci), session=${currentSessionId?.slice(-12) ?? "?"}`,
|
|
1578
1348
|
"info",
|
|
1579
1349
|
);
|
|
1580
1350
|
if (!navUserMsgId) {
|
|
1581
|
-
ctx.ui.notify(
|
|
1351
|
+
ctx.ui.notify("[SQ-debug] session_tree: 未找到 nav user msg", "info");
|
|
1582
1352
|
return;
|
|
1583
1353
|
}
|
|
1584
1354
|
// 查找队列中对应的 entry
|
|
@@ -1591,17 +1361,17 @@ export default function (pi: ExtensionAPI) {
|
|
|
1591
1361
|
// 但 currentIndex 永远 = entries.length-1,所以 idx >= currentIndex 的情形是“未真正导航”。
|
|
1592
1362
|
// 同样:idx > entries.length-1 的越界情形跳过。
|
|
1593
1363
|
if (idx < 0) {
|
|
1594
|
-
ctx.ui.notify(
|
|
1364
|
+
ctx.ui.notify(`[SQ-debug] session_tree: nav=${navUserMsgId?.slice(0, 8) ?? "?"} 未匹配 entries=${queue.entries.map(e => (e.sessionEntryId || '?').slice(0, 8)).join(',')}`, "info");
|
|
1595
1365
|
return;
|
|
1596
1366
|
}
|
|
1597
1367
|
// Option C 语义:entry 是检查点。“导航到 q3” 包含 “q3 本身被撤销”(q3 不再在检查点列表中)。
|
|
1598
1368
|
// 所以即使 idx === currentIndex(队列中最后一个 entry),仍然需要 fire。
|
|
1599
1369
|
// 唯一需要跳过的是 idx 超出 entries 范围(不可能访问的检查点)。
|
|
1600
1370
|
if (idx >= queue.entries.length) {
|
|
1601
|
-
ctx.ui.notify(
|
|
1371
|
+
ctx.ui.notify(`[SQ-debug] session_tree: idx=${idx} >= ${queue.entries.length}(超出队列范围),跳过`, "info");
|
|
1602
1372
|
return;
|
|
1603
1373
|
}
|
|
1604
|
-
ctx.ui.notify(
|
|
1374
|
+
ctx.ui.notify(`[SQ-debug] session_tree 回滚 idx=${idx} entries=${queue.entries.length}`, "info");
|
|
1605
1375
|
isProcessing = true;
|
|
1606
1376
|
try {
|
|
1607
1377
|
autoRollbackForSessionTree(ctx, queue, idx);
|
|
@@ -1609,7 +1379,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
1609
1379
|
isProcessing = false;
|
|
1610
1380
|
}
|
|
1611
1381
|
} catch (e: any) {
|
|
1612
|
-
ctx.ui.notify(
|
|
1382
|
+
ctx.ui.notify(`[SQ-debug] session_tree 异常: ${e.message}`, "warning");
|
|
1613
1383
|
}
|
|
1614
1384
|
});
|
|
1615
1385
|
|
|
@@ -1625,28 +1395,10 @@ export default function (pi: ExtensionAPI) {
|
|
|
1625
1395
|
|
|
1626
1396
|
// ── 命令:/rollback(单入口菜单) ──
|
|
1627
1397
|
pi.registerCommand("rollback", {
|
|
1628
|
-
description:
|
|
1398
|
+
description: "会话队列回滚(单入口菜单)",
|
|
1629
1399
|
handler: async (args, ctx) => {
|
|
1630
|
-
// 子命令:/rollback zh | /rollback en 切换语言
|
|
1631
|
-
const sub = args.trim().toLowerCase();
|
|
1632
|
-
if (sub === "zh" || sub === "en") {
|
|
1633
|
-
const config = loadConfig();
|
|
1634
|
-
config.lang = sub;
|
|
1635
|
-
saveConfig(config);
|
|
1636
|
-
setLang(sub);
|
|
1637
|
-
if (ctx.hasUI) {
|
|
1638
|
-
ctx.ui.setStatus("session-queue", getStatusText());
|
|
1639
|
-
ctx.ui.notify(t("notify.langSwitched", sub), "success");
|
|
1640
|
-
}
|
|
1641
|
-
return;
|
|
1642
|
-
}
|
|
1643
|
-
if (sub.length > 0 && sub !== "zh" && sub !== "en") {
|
|
1644
|
-
if (ctx.hasUI) ctx.ui.notify(t("notify.langInvalid", args.trim()), "warning");
|
|
1645
|
-
return;
|
|
1646
|
-
}
|
|
1647
|
-
|
|
1648
1400
|
if (!ctx.hasUI) {
|
|
1649
|
-
ctx.ui.notify(
|
|
1401
|
+
ctx.ui.notify("rollback 需要交互模式", "error");
|
|
1650
1402
|
return;
|
|
1651
1403
|
}
|
|
1652
1404
|
|
|
@@ -1658,17 +1410,17 @@ export default function (pi: ExtensionAPI) {
|
|
|
1658
1410
|
const config = loadConfig();
|
|
1659
1411
|
|
|
1660
1412
|
const mainOptions: string[] = [
|
|
1661
|
-
|
|
1662
|
-
|
|
1663
|
-
|
|
1664
|
-
activeWorkspace ?
|
|
1665
|
-
|
|
1666
|
-
|
|
1667
|
-
|
|
1413
|
+
`📋 回滚历史 (${Math.max(0, queueSize - 1)} 个变更点)`,
|
|
1414
|
+
"📂 管理工作区",
|
|
1415
|
+
"🗑️ 清空当前队列",
|
|
1416
|
+
activeWorkspace ? "⏸ 删除并清空" : "✅ 启用当前工作区",
|
|
1417
|
+
`🔗 跟随 Session Tree (${followSessionTreeEnabled ? "开" : "关"})`,
|
|
1418
|
+
"🧹 回收孤儿快照",
|
|
1419
|
+
"❌ 退出菜单",
|
|
1668
1420
|
];
|
|
1669
1421
|
|
|
1670
1422
|
const mainChoice = await ctx.ui.select(
|
|
1671
|
-
|
|
1423
|
+
"会话队列回滚",
|
|
1672
1424
|
mainOptions,
|
|
1673
1425
|
);
|
|
1674
1426
|
if (!mainChoice || mainChoice.startsWith("❌")) return;
|
|
@@ -1687,11 +1439,11 @@ export default function (pi: ExtensionAPI) {
|
|
|
1687
1439
|
} else if (mainChoice.startsWith("🗑️")) {
|
|
1688
1440
|
// 清空当前队列
|
|
1689
1441
|
if (!currentSessionId) {
|
|
1690
|
-
ctx.ui.notify(
|
|
1442
|
+
ctx.ui.notify("无活跃 session", "info");
|
|
1691
1443
|
continue;
|
|
1692
1444
|
}
|
|
1693
1445
|
if (queueSize === 0) {
|
|
1694
|
-
ctx.ui.notify(
|
|
1446
|
+
ctx.ui.notify("队列已经为空", "info");
|
|
1695
1447
|
continue;
|
|
1696
1448
|
}
|
|
1697
1449
|
const clearedQueue = loadQueue(currentSessionId);
|
|
@@ -1707,14 +1459,14 @@ export default function (pi: ExtensionAPI) {
|
|
|
1707
1459
|
// 清空后立即 GC,回收该 session 引用的孤儿快照
|
|
1708
1460
|
const gc = runGc();
|
|
1709
1461
|
ctx.ui.notify(
|
|
1710
|
-
|
|
1462
|
+
`✅ 队列已清空,回收 ${gc.snapDeleted} 个孤儿快照${gc.queueDeleted ? `,清理 ${gc.queueDeleted} 个旧 queue 文件` : ""}`,
|
|
1711
1463
|
"success",
|
|
1712
1464
|
);
|
|
1713
1465
|
// 回到主菜单
|
|
1714
1466
|
} else if (mainChoice.startsWith("⏸")) {
|
|
1715
1467
|
// 删除并清空
|
|
1716
1468
|
if (!activeWorkspace) {
|
|
1717
|
-
ctx.ui.notify(
|
|
1469
|
+
ctx.ui.notify("当前工作区未启用", "info");
|
|
1718
1470
|
continue;
|
|
1719
1471
|
}
|
|
1720
1472
|
removeWorkspace(ctx.cwd);
|
|
@@ -1730,21 +1482,21 @@ export default function (pi: ExtensionAPI) {
|
|
|
1730
1482
|
// 删除 queue.json 后 GC 回收该 session 独占的孤儿快照
|
|
1731
1483
|
const gc = runGc();
|
|
1732
1484
|
ctx.ui.notify(
|
|
1733
|
-
|
|
1485
|
+
`⏸ 已删除并清空当前 session 的记录,回收 ${gc.snapDeleted} 个孤儿快照${gc.queueDeleted ? `,清理 ${gc.queueDeleted} 个旧 queue 文件` : ""}`,
|
|
1734
1486
|
"success",
|
|
1735
1487
|
);
|
|
1736
1488
|
return; // 状态已变,退出菜单
|
|
1737
1489
|
} else if (mainChoice.startsWith("✅")) {
|
|
1738
1490
|
// 启用当前工作区
|
|
1739
1491
|
if (activeWorkspace) {
|
|
1740
|
-
ctx.ui.notify(
|
|
1492
|
+
ctx.ui.notify("当前工作区已启用", "info");
|
|
1741
1493
|
continue;
|
|
1742
1494
|
}
|
|
1743
1495
|
const resolved = path.resolve(ctx.cwd);
|
|
1744
1496
|
addWorkspace(resolved);
|
|
1745
1497
|
activeWorkspace = resolved;
|
|
1746
1498
|
ctx.ui.setStatus("session-queue", getStatusText());
|
|
1747
|
-
ctx.ui.notify(
|
|
1499
|
+
ctx.ui.notify(`✅ 已启用变更记录:${resolved}`, "success");
|
|
1748
1500
|
return; // 状态已变,退出菜单
|
|
1749
1501
|
} else if (mainChoice.startsWith("🔗")) {
|
|
1750
1502
|
// 切换 Session Tree 跟随回滚开关
|
|
@@ -1761,7 +1513,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
1761
1513
|
);
|
|
1762
1514
|
}
|
|
1763
1515
|
ctx.ui.notify(
|
|
1764
|
-
|
|
1516
|
+
followSessionTreeEnabled ? "🔗 已开启:Session Tree 导航将自动回滚队列" : "⏸ 已关闭:Session Tree 导航不会自动回滚队列",
|
|
1765
1517
|
"info",
|
|
1766
1518
|
);
|
|
1767
1519
|
// 同步更新状态栏文案
|
|
@@ -1771,7 +1523,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
1771
1523
|
// 手动 GC
|
|
1772
1524
|
const gc = runGc();
|
|
1773
1525
|
ctx.ui.notify(
|
|
1774
|
-
|
|
1526
|
+
`🧹 扫描 ${gc.snapScanned} 个快照,删除 ${gc.snapDeleted} 个孤儿,存活 ${gc.live} 个${gc.queueDeleted ? `;清理 ${gc.queueDeleted} 个旧 queue 文件,保留 ${gc.queueKept ?? 0} 个` : ""}`,
|
|
1775
1527
|
"success",
|
|
1776
1528
|
);
|
|
1777
1529
|
}
|
|
@@ -1788,24 +1540,24 @@ async function manageWorkspaces(ctx: any, _config: Config): Promise<boolean> {
|
|
|
1788
1540
|
// 一级:列出已启用的工作区
|
|
1789
1541
|
const config = loadConfig();
|
|
1790
1542
|
if (config.workspaces.length === 0) {
|
|
1791
|
-
ctx.ui.notify(
|
|
1543
|
+
ctx.ui.notify("没有已启用的工作区", "info");
|
|
1792
1544
|
return false;
|
|
1793
1545
|
}
|
|
1794
1546
|
|
|
1795
1547
|
// 用绝对路径 + 标记当前激活
|
|
1796
1548
|
const wsOptions = config.workspaces.map((ws) => {
|
|
1797
1549
|
const isCurrent = activeWorkspace && path.resolve(ws) === activeWorkspace;
|
|
1798
|
-
const tag = isCurrent ?
|
|
1550
|
+
const tag = isCurrent ? " ← 当前" : "";
|
|
1799
1551
|
return `${ws}${tag}`;
|
|
1800
1552
|
});
|
|
1801
|
-
wsOptions.push(
|
|
1553
|
+
wsOptions.push("← 返回");
|
|
1802
1554
|
|
|
1803
1555
|
const wsChoice = await ctx.ui.select(
|
|
1804
|
-
|
|
1556
|
+
`已启用的工作区 (${config.workspaces.length})`,
|
|
1805
1557
|
wsOptions,
|
|
1806
1558
|
);
|
|
1807
1559
|
|
|
1808
|
-
if (!wsChoice || wsChoice ===
|
|
1560
|
+
if (!wsChoice || wsChoice === "← 返回") return false;
|
|
1809
1561
|
|
|
1810
1562
|
// 从选项中提取路径(精确匹配)
|
|
1811
1563
|
const selectedWs = config.workspaces.find((ws) => wsChoice.startsWith(ws));
|
|
@@ -1815,19 +1567,19 @@ async function manageWorkspaces(ctx: any, _config: Config): Promise<boolean> {
|
|
|
1815
1567
|
const isCurrent = activeWorkspace && path.resolve(selectedWs) === activeWorkspace;
|
|
1816
1568
|
const opOptions: string[] = [];
|
|
1817
1569
|
if (isCurrent) {
|
|
1818
|
-
opOptions.push(
|
|
1570
|
+
opOptions.push("⏸ 暂停此工作区");
|
|
1819
1571
|
} else {
|
|
1820
|
-
opOptions.push(
|
|
1572
|
+
opOptions.push("✅ 启用此工作区");
|
|
1821
1573
|
}
|
|
1822
|
-
opOptions.push(
|
|
1823
|
-
opOptions.push(
|
|
1574
|
+
opOptions.push("🗑️ 从列表中删除");
|
|
1575
|
+
opOptions.push("← 返回");
|
|
1824
1576
|
|
|
1825
1577
|
const opChoice = await ctx.ui.select(
|
|
1826
|
-
|
|
1578
|
+
`工作区: ${selectedWs}`,
|
|
1827
1579
|
opOptions,
|
|
1828
1580
|
);
|
|
1829
1581
|
|
|
1830
|
-
if (!opChoice || opChoice ===
|
|
1582
|
+
if (!opChoice || opChoice === "← 返回") return false;
|
|
1831
1583
|
|
|
1832
1584
|
if (opChoice.startsWith("⏸")) {
|
|
1833
1585
|
// 暂停
|
|
@@ -1840,7 +1592,7 @@ async function manageWorkspaces(ctx: any, _config: Config): Promise<boolean> {
|
|
|
1840
1592
|
}
|
|
1841
1593
|
const gc = runGc();
|
|
1842
1594
|
ctx.ui.notify(
|
|
1843
|
-
|
|
1595
|
+
`⏸ 已暂停:${selectedWs},回收 ${gc.snapDeleted} 个孤儿快照${gc.queueDeleted ? `,清理 ${gc.queueDeleted} 个旧 queue 文件` : ""}`,
|
|
1844
1596
|
"success",
|
|
1845
1597
|
);
|
|
1846
1598
|
return true;
|
|
@@ -1851,10 +1603,10 @@ async function manageWorkspaces(ctx: any, _config: Config): Promise<boolean> {
|
|
|
1851
1603
|
addWorkspace(ctx.cwd);
|
|
1852
1604
|
activeWorkspace = path.resolve(ctx.cwd);
|
|
1853
1605
|
ctx.ui.setStatus("session-queue", getStatusText());
|
|
1854
|
-
ctx.ui.notify(
|
|
1606
|
+
ctx.ui.notify(`✅ 已启用:${selectedWs}`, "success");
|
|
1855
1607
|
return true;
|
|
1856
1608
|
} else {
|
|
1857
|
-
ctx.ui.notify(
|
|
1609
|
+
ctx.ui.notify("已有其他工作区启用中,请先暂停", "warning");
|
|
1858
1610
|
}
|
|
1859
1611
|
} else if (opChoice.startsWith("🗑️")) {
|
|
1860
1612
|
// 删除(从 config 移除)
|
|
@@ -1865,7 +1617,7 @@ async function manageWorkspaces(ctx: any, _config: Config): Promise<boolean> {
|
|
|
1865
1617
|
activeWorkspace = null;
|
|
1866
1618
|
ctx.ui.setStatus("session-queue", undefined);
|
|
1867
1619
|
}
|
|
1868
|
-
ctx.ui.notify(
|
|
1620
|
+
ctx.ui.notify(`🗑️ 已从列表中删除:${selectedWs}`, "success");
|
|
1869
1621
|
return false; // 状态未变(只是配置变了),可继续菜单
|
|
1870
1622
|
}
|
|
1871
1623
|
|
package/package.json
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@liziy/session-queue",
|
|
3
|
-
"version": "0.2.
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "0.2.2",
|
|
4
|
+
"description": "线性队列式回滚 · 文件快照一键还原",
|
|
5
5
|
"keywords": [
|
|
6
|
+
"pi-extension",
|
|
6
7
|
"pi-package",
|
|
7
8
|
"session",
|
|
8
9
|
"queue",
|
|
@@ -10,8 +11,7 @@
|
|
|
10
11
|
"file-snapshot",
|
|
11
12
|
"checkpoint",
|
|
12
13
|
"linear-history",
|
|
13
|
-
"memory"
|
|
14
|
-
"i18n"
|
|
14
|
+
"memory"
|
|
15
15
|
],
|
|
16
16
|
"license": "MIT",
|
|
17
17
|
"main": "index.ts",
|
|
@@ -20,10 +20,22 @@
|
|
|
20
20
|
"README.md"
|
|
21
21
|
],
|
|
22
22
|
"pi": {
|
|
23
|
-
"extensions": [
|
|
23
|
+
"extensions": [
|
|
24
|
+
"./index.ts"
|
|
25
|
+
]
|
|
24
26
|
},
|
|
25
27
|
"peerDependencies": {
|
|
26
28
|
"@earendil-works/pi-coding-agent": "*",
|
|
27
29
|
"@earendil-works/pi-tui": "*"
|
|
28
|
-
}
|
|
30
|
+
},
|
|
31
|
+
"repository": {
|
|
32
|
+
"type": "git",
|
|
33
|
+
"url": "git+https://github.com/yu-d1/pi-extensions.git",
|
|
34
|
+
"directory": "packages/session-queue"
|
|
35
|
+
},
|
|
36
|
+
"homepage": "https://github.com/yu-d1/pi-extensions/tree/main/packages/session-queue#readme",
|
|
37
|
+
"bugs": {
|
|
38
|
+
"url": "https://github.com/yu-d1/pi-extensions/issues"
|
|
39
|
+
},
|
|
40
|
+
"author": "liziy"
|
|
29
41
|
}
|