@jingyi0605/codingns 0.3.6 → 0.4.0

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.
Files changed (185) hide show
  1. package/README.md +3 -0
  2. package/bin/codingns.mjs +489 -1
  3. package/dist/public/assets/{TerminalPage-D00S4KM6.js → TerminalPage-6jHZV9Mh.js} +17 -17
  4. package/dist/public/assets/index-CSVhg7I8.js +123 -0
  5. package/dist/public/assets/index-Ce1VX19m.css +1 -0
  6. package/dist/public/index.html +2 -2
  7. package/dist/server/modules/assistant-capability/assistant-capability-controller.d.ts +173 -0
  8. package/dist/server/modules/assistant-capability/assistant-capability-controller.js +307 -0
  9. package/dist/server/modules/assistant-capability/assistant-capability-controller.js.map +1 -1
  10. package/dist/server/modules/assistant-capability/assistant-capability-service.d.ts +199 -2
  11. package/dist/server/modules/assistant-capability/assistant-capability-service.js +565 -3
  12. package/dist/server/modules/assistant-capability/assistant-capability-service.js.map +1 -1
  13. package/dist/server/modules/butler/assistant-automation-service.d.ts +110 -0
  14. package/dist/server/modules/butler/assistant-automation-service.js +786 -0
  15. package/dist/server/modules/butler/assistant-automation-service.js.map +1 -0
  16. package/dist/server/modules/butler/assistant-automation-trigger.d.ts +94 -0
  17. package/dist/server/modules/butler/assistant-automation-trigger.js +400 -0
  18. package/dist/server/modules/butler/assistant-automation-trigger.js.map +1 -0
  19. package/dist/server/modules/butler/assistant-sandbox-service.d.ts +55 -0
  20. package/dist/server/modules/butler/assistant-sandbox-service.js +266 -0
  21. package/dist/server/modules/butler/assistant-sandbox-service.js.map +1 -0
  22. package/dist/server/modules/butler/butler-action-context-service.d.ts +4 -1
  23. package/dist/server/modules/butler/butler-action-context-service.js +8 -2
  24. package/dist/server/modules/butler/butler-action-context-service.js.map +1 -1
  25. package/dist/server/modules/butler/butler-control-session-service.d.ts +8 -1
  26. package/dist/server/modules/butler/butler-control-session-service.js +154 -40
  27. package/dist/server/modules/butler/butler-control-session-service.js.map +1 -1
  28. package/dist/server/modules/butler/butler-control-timer-scheduler.d.ts +32 -0
  29. package/dist/server/modules/butler/butler-control-timer-scheduler.js +93 -0
  30. package/dist/server/modules/butler/butler-control-timer-scheduler.js.map +1 -0
  31. package/dist/server/modules/butler/butler-control-timer-service.d.ts +42 -0
  32. package/dist/server/modules/butler/butler-control-timer-service.js +132 -0
  33. package/dist/server/modules/butler/butler-control-timer-service.js.map +1 -0
  34. package/dist/server/modules/butler/butler-controller.d.ts +42 -2
  35. package/dist/server/modules/butler/butler-controller.js +79 -12
  36. package/dist/server/modules/butler/butler-controller.js.map +1 -1
  37. package/dist/server/modules/butler/butler-follow-up-service.d.ts +9 -1
  38. package/dist/server/modules/butler/butler-follow-up-service.js +273 -181
  39. package/dist/server/modules/butler/butler-follow-up-service.js.map +1 -1
  40. package/dist/server/modules/butler/butler-inbox-analysis-service.d.ts +4 -1
  41. package/dist/server/modules/butler/butler-inbox-analysis-service.js +18 -4
  42. package/dist/server/modules/butler/butler-inbox-analysis-service.js.map +1 -1
  43. package/dist/server/modules/butler/butler-profile-service.js +2 -5
  44. package/dist/server/modules/butler/butler-profile-service.js.map +1 -1
  45. package/dist/server/modules/butler/butler-project-service.d.ts +3 -1
  46. package/dist/server/modules/butler/butler-project-service.js +7 -1
  47. package/dist/server/modules/butler/butler-project-service.js.map +1 -1
  48. package/dist/server/modules/butler/butler-session-service.d.ts +3 -1
  49. package/dist/server/modules/butler/butler-session-service.js +12 -1
  50. package/dist/server/modules/butler/butler-session-service.js.map +1 -1
  51. package/dist/server/modules/butler/butler-session-summary-service.js +2 -1
  52. package/dist/server/modules/butler/butler-session-summary-service.js.map +1 -1
  53. package/dist/server/modules/butler/butler-workspace-context.d.ts +3 -0
  54. package/dist/server/modules/butler/butler-workspace-context.js +164 -44
  55. package/dist/server/modules/butler/butler-workspace-context.js.map +1 -1
  56. package/dist/server/modules/butler/patrol-execution-service.js +2 -1
  57. package/dist/server/modules/butler/patrol-execution-service.js.map +1 -1
  58. package/dist/server/modules/butler/provider-adapter-registry.d.ts +3 -0
  59. package/dist/server/modules/butler/provider-adapter-registry.js +18 -1
  60. package/dist/server/modules/butler/provider-adapter-registry.js.map +1 -1
  61. package/dist/server/modules/butler/verification-run-service.d.ts +9 -2
  62. package/dist/server/modules/butler/verification-run-service.js +188 -34
  63. package/dist/server/modules/butler/verification-run-service.js.map +1 -1
  64. package/dist/server/modules/debug-target/debug-target-controller.js +1 -1
  65. package/dist/server/modules/debug-target/debug-target-controller.js.map +1 -1
  66. package/dist/server/modules/debug-target/debug-target-service.d.ts +7 -2
  67. package/dist/server/modules/debug-target/debug-target-service.js +563 -100
  68. package/dist/server/modules/debug-target/debug-target-service.js.map +1 -1
  69. package/dist/server/modules/git/git-command-helper-client.d.ts +1 -0
  70. package/dist/server/modules/git/git-command-helper-client.js +19 -26
  71. package/dist/server/modules/git/git-command-helper-client.js.map +1 -1
  72. package/dist/server/modules/git/git-command-runner.js +19 -1
  73. package/dist/server/modules/git/git-command-runner.js.map +1 -1
  74. package/dist/server/modules/preferences/profile-service.d.ts +3 -1
  75. package/dist/server/modules/preferences/profile-service.js +74 -3
  76. package/dist/server/modules/preferences/profile-service.js.map +1 -1
  77. package/dist/server/modules/provider/provider-discovery-helper-client.d.ts +5 -3
  78. package/dist/server/modules/provider/provider-discovery-helper-client.js +129 -43
  79. package/dist/server/modules/provider/provider-discovery-helper-client.js.map +1 -1
  80. package/dist/server/modules/provider/provider-discovery-helper-process.js +44 -0
  81. package/dist/server/modules/provider/provider-discovery-helper-process.js.map +1 -1
  82. package/dist/server/modules/provider/provider-discovery-runtime.js +83 -3
  83. package/dist/server/modules/provider/provider-discovery-runtime.js.map +1 -1
  84. package/dist/server/modules/sessions/claude-runtime-helper-client.js +23 -1
  85. package/dist/server/modules/sessions/claude-runtime-helper-client.js.map +1 -1
  86. package/dist/server/modules/sessions/session-history-service.d.ts +7 -1
  87. package/dist/server/modules/sessions/session-history-service.js +251 -41
  88. package/dist/server/modules/sessions/session-history-service.js.map +1 -1
  89. package/dist/server/modules/sessions/session-live-runtime-service.d.ts +6 -0
  90. package/dist/server/modules/sessions/session-live-runtime-service.js +97 -11
  91. package/dist/server/modules/sessions/session-live-runtime-service.js.map +1 -1
  92. package/dist/server/modules/sessions/session-message-origin-utils.d.ts +12 -0
  93. package/dist/server/modules/sessions/session-message-origin-utils.js +45 -0
  94. package/dist/server/modules/sessions/session-message-origin-utils.js.map +1 -0
  95. package/dist/server/modules/sessions/session-permission-request-service.js +167 -0
  96. package/dist/server/modules/sessions/session-permission-request-service.js.map +1 -1
  97. package/dist/server/modules/skills/builtin-skill-service.js +1 -1
  98. package/dist/server/modules/skills/builtin-skill-service.js.map +1 -1
  99. package/dist/server/modules/skills/builtin-skills/codingns-assistant/SKILL.md +19 -12
  100. package/dist/server/modules/skills/builtin-skills/codingns-assistant/references/cli-workflow.md +9 -3
  101. package/dist/server/modules/tasks/task-helper-client.d.ts +5 -2
  102. package/dist/server/modules/tasks/task-helper-client.js +118 -38
  103. package/dist/server/modules/tasks/task-helper-client.js.map +1 -1
  104. package/dist/server/modules/tasks/task-helper-process.js +94 -3
  105. package/dist/server/modules/tasks/task-helper-process.js.map +1 -1
  106. package/dist/server/modules/tasks/task-types.d.ts +3 -0
  107. package/dist/server/modules/tasks/task-types.js +4 -1
  108. package/dist/server/modules/tasks/task-types.js.map +1 -1
  109. package/dist/server/modules/terminal/command-template-service.d.ts +9 -0
  110. package/dist/server/modules/terminal/command-template-service.js +87 -5
  111. package/dist/server/modules/terminal/command-template-service.js.map +1 -1
  112. package/dist/server/modules/terminal/terminal-controller.d.ts +3 -0
  113. package/dist/server/modules/terminal/terminal-controller.js +41 -0
  114. package/dist/server/modules/terminal/terminal-controller.js.map +1 -1
  115. package/dist/server/modules/workbench/workbench-service.d.ts +3 -0
  116. package/dist/server/modules/workbench/workbench-service.js +4 -3
  117. package/dist/server/modules/workbench/workbench-service.js.map +1 -1
  118. package/dist/server/modules/workbench/workspace-file-watcher.d.ts +14 -6
  119. package/dist/server/modules/workbench/workspace-file-watcher.js +267 -57
  120. package/dist/server/modules/workbench/workspace-file-watcher.js.map +1 -1
  121. package/dist/server/modules/workbench/workspace-panel-snapshot-service.d.ts +2 -0
  122. package/dist/server/modules/workbench/workspace-panel-snapshot-service.js +32 -3
  123. package/dist/server/modules/workbench/workspace-panel-snapshot-service.js.map +1 -1
  124. package/dist/server/modules/worktree/worktree-manager.d.ts +9 -1
  125. package/dist/server/modules/worktree/worktree-manager.js +9 -1
  126. package/dist/server/modules/worktree/worktree-manager.js.map +1 -1
  127. package/dist/server/routes/assistant.js +19 -0
  128. package/dist/server/routes/assistant.js.map +1 -1
  129. package/dist/server/routes/butler.js +5 -0
  130. package/dist/server/routes/butler.js.map +1 -1
  131. package/dist/server/server/create-server.d.ts +8 -0
  132. package/dist/server/server/create-server.js +36 -13
  133. package/dist/server/server/create-server.js.map +1 -1
  134. package/dist/server/storage/repositories/assistant-automation-run-repository.d.ts +12 -0
  135. package/dist/server/storage/repositories/assistant-automation-run-repository.js +139 -0
  136. package/dist/server/storage/repositories/assistant-automation-run-repository.js.map +1 -0
  137. package/dist/server/storage/repositories/assistant-automation-task-repository.d.ts +15 -0
  138. package/dist/server/storage/repositories/assistant-automation-task-repository.js +173 -0
  139. package/dist/server/storage/repositories/assistant-automation-task-repository.js.map +1 -0
  140. package/dist/server/storage/repositories/assistant-sandbox-workspace-repository.d.ts +17 -0
  141. package/dist/server/storage/repositories/assistant-sandbox-workspace-repository.js +164 -0
  142. package/dist/server/storage/repositories/assistant-sandbox-workspace-repository.js.map +1 -0
  143. package/dist/server/storage/repositories/butler-control-session-repository.js +27 -3
  144. package/dist/server/storage/repositories/butler-control-session-repository.js.map +1 -1
  145. package/dist/server/storage/repositories/butler-control-timer-repository.d.ts +15 -0
  146. package/dist/server/storage/repositories/butler-control-timer-repository.js +157 -0
  147. package/dist/server/storage/repositories/butler-control-timer-repository.js.map +1 -0
  148. package/dist/server/storage/repositories/user-preference-profile-repository.js +6 -3
  149. package/dist/server/storage/repositories/user-preference-profile-repository.js.map +1 -1
  150. package/dist/server/storage/sqlite/client.js +239 -2
  151. package/dist/server/storage/sqlite/client.js.map +1 -1
  152. package/dist/server/storage/sqlite/schema.sql +107 -1
  153. package/dist/server/types/domain.d.ts +89 -2
  154. package/dist/server/ws/workbench-ws-hub.d.ts +14 -8
  155. package/dist/server/ws/workbench-ws-hub.js +299 -163
  156. package/dist/server/ws/workbench-ws-hub.js.map +1 -1
  157. package/node_modules/@codingns/session-sync-core/dist/providers/claude-code.d.ts +4 -1
  158. package/node_modules/@codingns/session-sync-core/dist/providers/claude-code.js +111 -3
  159. package/node_modules/@codingns/session-sync-core/dist/providers/claude-code.js.map +1 -1
  160. package/node_modules/@codingns/session-sync-core/dist/providers/codex.d.ts +6 -1
  161. package/node_modules/@codingns/session-sync-core/dist/providers/codex.js +306 -31
  162. package/node_modules/@codingns/session-sync-core/dist/providers/codex.js.map +1 -1
  163. package/node_modules/@codingns/session-sync-core/dist/providers/gemini.d.ts +5 -1
  164. package/node_modules/@codingns/session-sync-core/dist/providers/gemini.js +187 -26
  165. package/node_modules/@codingns/session-sync-core/dist/providers/gemini.js.map +1 -1
  166. package/node_modules/@codingns/session-sync-core/dist/providers/kimi.d.ts +4 -1
  167. package/node_modules/@codingns/session-sync-core/dist/providers/kimi.js +98 -1
  168. package/node_modules/@codingns/session-sync-core/dist/providers/kimi.js.map +1 -1
  169. package/node_modules/@codingns/session-sync-core/dist/providers/opencode.d.ts +2 -0
  170. package/node_modules/@codingns/session-sync-core/dist/providers/opencode.js +71 -8
  171. package/node_modules/@codingns/session-sync-core/dist/providers/opencode.js.map +1 -1
  172. package/node_modules/@codingns/session-sync-core/dist/providers/utils.d.ts +1 -0
  173. package/node_modules/@codingns/session-sync-core/dist/providers/utils.js +4 -1
  174. package/node_modules/@codingns/session-sync-core/dist/providers/utils.js.map +1 -1
  175. package/node_modules/@codingns/session-sync-core/dist/runtime/claude-runtime.js +44 -0
  176. package/node_modules/@codingns/session-sync-core/dist/runtime/claude-runtime.js.map +1 -1
  177. package/node_modules/@codingns/session-sync-core/dist/runtime/codex-runtime.js +9 -3
  178. package/node_modules/@codingns/session-sync-core/dist/runtime/codex-runtime.js.map +1 -1
  179. package/node_modules/@codingns/session-sync-core/dist/runtime/types.d.ts +1 -0
  180. package/node_modules/@codingns/session-sync-core/dist/services.js +17 -8
  181. package/node_modules/@codingns/session-sync-core/dist/services.js.map +1 -1
  182. package/node_modules/@codingns/session-sync-core/dist/types.d.ts +4 -0
  183. package/package.json +1 -1
  184. package/dist/public/assets/index-BlOinYqR.js +0 -122
  185. package/dist/public/assets/index-Dg_7g6lA.css +0 -1
@@ -1,8 +1,10 @@
1
1
  import { AppError } from "../shared/errors/app-error.js";
2
2
  import { logTerminalDebug, terminalDebugNowMs } from "../shared/utils/terminal-debug-log.js";
3
3
  const WORKBENCH_REFRESH_INTERVAL_MS = 60_000;
4
- const SIDEBAR_REFRESH_INTERVAL_MS = 5_000;
5
4
  const GIT_SUBSCRIPTION_MIN_REFRESH_INTERVAL_MS = 15_000;
5
+ const GIT_REFRESH_QUIET_WINDOW_MS = 800;
6
+ const TERMINAL_MANAGER_REFRESH_QUIET_WINDOW_MS = 300;
7
+ const WORKSPACE_MANAGEMENT_REFRESH_INTERVAL_MS = 5_000;
6
8
  const WORKBENCH_REALTIME_BROADCAST_DEBOUNCE_MS = 120;
7
9
  const WORKSPACE_MANAGEMENT_TIMER_REFRESH_ENABLED = readBooleanEnv(process.env.CODINGNS_WORKSPACE_MANAGEMENT_AUTO_REFRESH, false);
8
10
  export class WorkbenchWsHub {
@@ -15,12 +17,18 @@ export class WorkbenchWsHub {
15
17
  clientGitSubscriptions = new WeakMap();
16
18
  clientTerminalManagerSubscriptions = new WeakMap();
17
19
  clientWorkspaceManagementSubscriptions = new WeakMap();
18
- constructor(workbenchService, workspacePanelSnapshotService, fileWatcher) {
20
+ constructor(workbenchService, workspacePanelSnapshotService, fileWatcher, terminalService) {
19
21
  this.workbenchService = workbenchService;
20
22
  this.workspacePanelSnapshotService = workspacePanelSnapshotService;
21
23
  this.fileWatcher = fileWatcher;
22
- this.fileWatcher.setOnChange((workspaceId) => {
23
- this.handleWorkspaceFileChange(workspaceId);
24
+ this.fileWatcher.setOnChange((event) => {
25
+ this.handleWorkspaceWatcherChange(event);
26
+ });
27
+ terminalService?.on("status", (terminal) => {
28
+ this.handleTerminalManagerChange(terminal.workspaceId);
29
+ });
30
+ terminalService?.on("exit", ({ terminal }) => {
31
+ this.handleTerminalManagerChange(terminal.workspaceId);
24
32
  });
25
33
  }
26
34
  handleMessage(client, payload, authContext) {
@@ -34,18 +42,15 @@ export class WorkbenchWsHub {
34
42
  switch (message.type) {
35
43
  case "workbench.subscribe":
36
44
  void this.sendWorkbenchSnapshotToClient(client, userId, channel);
45
+ if (this.workbenchService.shouldRefreshSnapshot()) {
46
+ this.workbenchService.scheduleSnapshotRefresh(userId);
47
+ }
37
48
  return true;
38
49
  case "workbench.refresh":
39
- void this.syncTitlesAndBroadcast(userId);
40
50
  void this.refreshAndBroadcast(userId, true);
41
51
  return true;
42
52
  case "fileTree.subscribe":
43
- this.clientFileTreeSubscriptions.set(client, {
44
- workspaceId: message.workspaceId.trim(),
45
- paths: normalizePanelPaths(message.paths),
46
- lastPayloadByPath: new Map()
47
- });
48
- this.fileWatcher.subscribe(message.workspaceId.trim());
53
+ this.replaceFileTreeSubscription(client, message.workspaceId, message.paths);
49
54
  void this.refreshFileTreeSubscriptions(client);
50
55
  return true;
51
56
  case "fileTree.refresh":
@@ -56,42 +61,29 @@ export class WorkbenchWsHub {
56
61
  void this.refreshFileTreeSubscriptions(client, true);
57
62
  return true;
58
63
  case "git.subscribe":
59
- this.clientGitSubscriptions.set(client, {
60
- workspaceId: message.workspaceId.trim(),
61
- lastPayload: null,
62
- lastRequestedAt: 0,
63
- refreshTask: null,
64
- refreshController: null
64
+ this.ensureGitSubscription(client, message.workspaceId);
65
+ void this.refreshGitSubscription(client, false, {
66
+ deliverIfUnchanged: true,
67
+ ignoreMinInterval: true
65
68
  });
66
- this.fileWatcher.subscribe(message.workspaceId.trim());
67
- void this.refreshGitSubscription(client);
68
69
  return true;
69
70
  case "git.refresh":
70
71
  this.workspacePanelSnapshotService.invalidateGit(message.workspaceId.trim());
71
- this.abortGitRefresh(client, "git subscription replaced");
72
- this.clientGitSubscriptions.set(client, {
73
- workspaceId: message.workspaceId.trim(),
74
- lastPayload: null,
75
- lastRequestedAt: 0,
76
- refreshTask: null,
77
- refreshController: null
72
+ this.ensureGitSubscription(client, message.workspaceId);
73
+ this.scheduleGitRefresh(client, {
74
+ force: true
78
75
  });
79
- void this.refreshGitSubscription(client, true);
80
76
  return true;
81
77
  case "terminalManager.subscribe":
82
- this.clientTerminalManagerSubscriptions.set(client, {
83
- workspaceId: message.workspaceId.trim(),
84
- lastPayload: null
85
- });
86
- void this.refreshTerminalManagerSubscription(client);
78
+ this.ensureTerminalManagerSubscription(client, message.workspaceId);
79
+ this.scheduleTerminalManagerRefresh(client);
87
80
  return true;
88
81
  case "terminalManager.refresh":
89
82
  this.workspacePanelSnapshotService.invalidateTerminalManager(message.workspaceId.trim());
90
- this.clientTerminalManagerSubscriptions.set(client, {
91
- workspaceId: message.workspaceId.trim(),
92
- lastPayload: null
83
+ this.ensureTerminalManagerSubscription(client, message.workspaceId);
84
+ this.scheduleTerminalManagerRefresh(client, {
85
+ force: true
93
86
  });
94
- void this.refreshTerminalManagerSubscription(client, true);
95
87
  return true;
96
88
  case "workspaceManagement.subscribe":
97
89
  this.clientWorkspaceManagementSubscriptions.set(client, {
@@ -127,12 +119,19 @@ export class WorkbenchWsHub {
127
119
  // 文件监听器引用计数递减
128
120
  const fileTreeSub = this.clientFileTreeSubscriptions.get(client);
129
121
  if (fileTreeSub) {
130
- this.fileWatcher.unsubscribe(fileTreeSub.workspaceId);
122
+ this.fileWatcher.unsubscribeFileTree(fileTreeSub.workspaceId, fileTreeSub.paths);
131
123
  }
132
124
  const gitSub = this.clientGitSubscriptions.get(client);
133
125
  if (gitSub) {
134
126
  gitSub.refreshController?.abort(new Error("git subscription closed"));
135
- this.fileWatcher.unsubscribe(gitSub.workspaceId);
127
+ if (gitSub.refreshTimer) {
128
+ clearTimeout(gitSub.refreshTimer);
129
+ }
130
+ this.fileWatcher.unsubscribeGit(gitSub.workspaceId);
131
+ }
132
+ const terminalManagerSub = this.clientTerminalManagerSubscriptions.get(client);
133
+ if (terminalManagerSub?.refreshTimer) {
134
+ clearTimeout(terminalManagerSub.refreshTimer);
136
135
  }
137
136
  this.clientFileTreeSubscriptions.delete(client);
138
137
  this.clientGitSubscriptions.delete(client);
@@ -144,8 +143,8 @@ export class WorkbenchWsHub {
144
143
  if (channel.workbenchTimer) {
145
144
  clearInterval(channel.workbenchTimer);
146
145
  }
147
- if (channel.sidebarTimer) {
148
- clearInterval(channel.sidebarTimer);
146
+ if (channel.workspaceManagementTimer) {
147
+ clearInterval(channel.workspaceManagementTimer);
149
148
  }
150
149
  if (channel.realtimeBroadcastTimer) {
151
150
  clearTimeout(channel.realtimeBroadcastTimer);
@@ -208,22 +207,42 @@ export class WorkbenchWsHub {
208
207
  channel.clients.add(client);
209
208
  this.clientUsers.set(client, userId);
210
209
  }
211
- /**
212
- * 文件变化回调:chokidar 检测到工作区文件变化后,失效缓存并推送更新给订阅了该工作区的客户端。
213
- */
214
- handleWorkspaceFileChange(workspaceId) {
215
- this.workspacePanelSnapshotService.invalidateFileTree(workspaceId);
216
- this.workspacePanelSnapshotService.invalidateGit(workspaceId);
210
+ handleWorkspaceWatcherChange(event) {
211
+ if (event.scope === "fileTree") {
212
+ this.workspacePanelSnapshotService.invalidateFileTree(event.workspaceId);
213
+ this.workspacePanelSnapshotService.invalidateGit(event.workspaceId);
214
+ }
215
+ else {
216
+ this.workspacePanelSnapshotService.invalidateGit(event.workspaceId);
217
+ }
217
218
  for (const [, channel] of this.userChannels) {
218
219
  for (const client of channel.clients) {
219
220
  const fileTreeSub = this.clientFileTreeSubscriptions.get(client);
220
- if (fileTreeSub && fileTreeSub.workspaceId === workspaceId) {
221
+ if (event.scope === "fileTree" &&
222
+ fileTreeSub &&
223
+ fileTreeSub.workspaceId === event.workspaceId) {
221
224
  void this.refreshFileTreeSubscriptions(client, true);
222
225
  }
223
226
  const gitSub = this.clientGitSubscriptions.get(client);
224
- if (gitSub && gitSub.workspaceId === workspaceId) {
225
- void this.refreshGitSubscription(client, true);
227
+ if (gitSub && gitSub.workspaceId === event.workspaceId) {
228
+ this.scheduleGitRefresh(client, {
229
+ quietWindowMs: GIT_REFRESH_QUIET_WINDOW_MS
230
+ });
231
+ }
232
+ }
233
+ }
234
+ }
235
+ handleTerminalManagerChange(workspaceId) {
236
+ this.workspacePanelSnapshotService.invalidateTerminalManager(workspaceId);
237
+ for (const [, channel] of this.userChannels) {
238
+ for (const client of channel.clients) {
239
+ const subscription = this.clientTerminalManagerSubscriptions.get(client);
240
+ if (!subscription || subscription.workspaceId !== workspaceId) {
241
+ continue;
226
242
  }
243
+ this.scheduleTerminalManagerRefresh(client, {
244
+ quietWindowMs: TERMINAL_MANAGER_REFRESH_QUIET_WINDOW_MS
245
+ });
227
246
  }
228
247
  }
229
248
  }
@@ -236,12 +255,11 @@ export class WorkbenchWsHub {
236
255
  clients: new Set(),
237
256
  lastWorkbenchPayload: null,
238
257
  workbenchTimer: null,
239
- sidebarTimer: null,
258
+ workspaceManagementTimer: null,
240
259
  realtimeBroadcastTimer: null,
241
260
  realtimeBroadcastQueued: false,
242
261
  realtimeBroadcastTask: null,
243
- refreshTask: null,
244
- titleSyncTask: null
262
+ refreshTask: null
245
263
  };
246
264
  channel.workbenchTimer = setInterval(() => {
247
265
  if (!this.workbenchService.shouldRefreshSnapshot()) {
@@ -251,47 +269,16 @@ export class WorkbenchWsHub {
251
269
  this.reportAsyncError("workbenchTimer", error, { userId });
252
270
  });
253
271
  }, WORKBENCH_REFRESH_INTERVAL_MS);
254
- channel.sidebarTimer = setInterval(() => {
255
- void this.refreshSidebarSubscriptions(userId).catch((error) => {
256
- this.reportAsyncError("sidebarTimer", error, { userId });
257
- });
258
- }, SIDEBAR_REFRESH_INTERVAL_MS);
272
+ if (WORKSPACE_MANAGEMENT_TIMER_REFRESH_ENABLED) {
273
+ channel.workspaceManagementTimer = setInterval(() => {
274
+ void this.refreshWorkspaceManagementSubscriptions(userId).catch((error) => {
275
+ this.reportAsyncError("workspaceManagementTimer", error, { userId });
276
+ });
277
+ }, WORKSPACE_MANAGEMENT_REFRESH_INTERVAL_MS);
278
+ }
259
279
  this.userChannels.set(userId, channel);
260
280
  return channel;
261
281
  }
262
- async syncTitlesAndBroadcast(userId) {
263
- const channel = this.getOrCreateChannel(userId);
264
- if (channel.titleSyncTask) {
265
- return channel.titleSyncTask;
266
- }
267
- channel.titleSyncTask = (async () => {
268
- const startedAtMs = terminalDebugNowMs();
269
- try {
270
- const snapshot = await this.workbenchService
271
- .scheduleSessionTitleSync(userId, "workbench_ws.sync_titles")
272
- .promise;
273
- const payload = buildWorkbenchPayload(snapshot);
274
- if (payload === channel.lastWorkbenchPayload) {
275
- return;
276
- }
277
- channel.lastWorkbenchPayload = payload;
278
- for (const client of channel.clients) {
279
- client.send(payload);
280
- }
281
- logTerminalDebug("workbench.sync_titles.completed", {
282
- userId,
283
- clientCount: channel.clients.size,
284
- durationMs: terminalDebugNowMs() - startedAtMs
285
- });
286
- }
287
- catch (error) {
288
- this.reportAsyncError("syncTitlesAndBroadcast", error, { userId });
289
- }
290
- })().finally(() => {
291
- channel.titleSyncTask = null;
292
- });
293
- return channel.titleSyncTask;
294
- }
295
282
  async sendWorkbenchSnapshotToClient(client, userId, channel) {
296
283
  try {
297
284
  const payload = buildWorkbenchPayload(this.workbenchService.getSnapshot(userId));
@@ -338,48 +325,30 @@ export class WorkbenchWsHub {
338
325
  });
339
326
  return channel.refreshTask;
340
327
  }
341
- async refreshSidebarSubscriptions(userId) {
342
- const channel = this.userChannels.get(userId);
343
- if (!channel) {
344
- return;
345
- }
346
- const startedAtMs = terminalDebugNowMs();
347
- await Promise.all([...channel.clients].map(async (client) => {
348
- const refreshTasks = [
349
- this.refreshGitSubscription(client),
350
- this.refreshTerminalManagerSubscription(client)
351
- ];
352
- if (WORKSPACE_MANAGEMENT_TIMER_REFRESH_ENABLED) {
353
- refreshTasks.push(this.refreshWorkspaceManagementSubscription(client));
354
- }
355
- await Promise.allSettled(refreshTasks);
356
- }));
357
- logTerminalDebug("workbench.sidebar_refresh.completed", {
358
- userId,
359
- clientCount: channel.clients.size,
360
- durationMs: terminalDebugNowMs() - startedAtMs
361
- });
362
- }
363
328
  ensureFileTreeSubscription(client, workspaceId, paths) {
364
329
  const current = this.clientFileTreeSubscriptions.get(client);
365
330
  const normalizedWorkspaceId = workspaceId.trim();
366
331
  const normalizedPaths = normalizePanelPaths(paths);
367
- if (!current || current.workspaceId !== normalizedWorkspaceId) {
368
- const next = {
369
- workspaceId: normalizedWorkspaceId,
370
- paths: normalizedPaths,
371
- lastPayloadByPath: new Map()
372
- };
373
- this.clientFileTreeSubscriptions.set(client, next);
374
- return next;
375
- }
376
- if (normalizedPaths.length > 0) {
377
- current.paths = normalizedPaths;
378
- }
379
- if (current.paths.length === 0) {
380
- current.paths = [""];
381
- }
382
- return current;
332
+ const nextPaths = normalizedPaths.length > 0 ? normalizedPaths : [""];
333
+ if (current &&
334
+ current.workspaceId === normalizedWorkspaceId &&
335
+ areStringArraysEqual(current.paths, nextPaths)) {
336
+ return current;
337
+ }
338
+ if (current) {
339
+ this.fileWatcher.unsubscribeFileTree(current.workspaceId, current.paths);
340
+ }
341
+ const next = {
342
+ workspaceId: normalizedWorkspaceId,
343
+ paths: nextPaths,
344
+ lastPayloadByPath: new Map()
345
+ };
346
+ this.clientFileTreeSubscriptions.set(client, next);
347
+ this.fileWatcher.subscribeFileTree(normalizedWorkspaceId, nextPaths);
348
+ return next;
349
+ }
350
+ replaceFileTreeSubscription(client, workspaceId, paths) {
351
+ return this.ensureFileTreeSubscription(client, workspaceId, paths);
383
352
  }
384
353
  async refreshFileTreeSubscriptions(client, force = false) {
385
354
  const subscription = this.clientFileTreeSubscriptions.get(client);
@@ -405,21 +374,30 @@ export class WorkbenchWsHub {
405
374
  });
406
375
  }
407
376
  }
408
- async refreshGitSubscription(client, force = false) {
377
+ async refreshGitSubscription(client, force = false, options) {
409
378
  const subscription = this.clientGitSubscriptions.get(client);
410
379
  if (!subscription) {
411
380
  return;
412
381
  }
413
382
  if (subscription.refreshTask) {
414
- if (!force) {
383
+ subscription.queuedRefresh = true;
384
+ subscription.queuedForce = subscription.queuedForce || force;
385
+ if (!options?.deliverIfUnchanged) {
415
386
  return subscription.refreshTask;
416
387
  }
417
- subscription.refreshController?.abort(new Error("git subscription superseded"));
418
- await subscription.refreshTask;
388
+ return await subscription.refreshTask.finally(() => {
389
+ if (subscription.lastPayload) {
390
+ client.send(subscription.lastPayload);
391
+ }
392
+ });
419
393
  }
420
394
  const now = Date.now();
421
- if (!force
395
+ if (!options?.ignoreMinInterval &&
396
+ !force
422
397
  && now - subscription.lastRequestedAt < GIT_SUBSCRIPTION_MIN_REFRESH_INTERVAL_MS) {
398
+ subscription.queuedRefresh = true;
399
+ subscription.queuedForce = subscription.queuedForce || force;
400
+ this.deferQueuedGitRefresh(client, GIT_SUBSCRIPTION_MIN_REFRESH_INTERVAL_MS - (now - subscription.lastRequestedAt));
423
401
  return;
424
402
  }
425
403
  subscription.lastRequestedAt = now;
@@ -436,7 +414,7 @@ export class WorkbenchWsHub {
436
414
  return;
437
415
  }
438
416
  const payload = buildGitPayload(snapshot);
439
- if (payload === subscription.lastPayload) {
417
+ if (payload === subscription.lastPayload && !options?.deliverIfUnchanged) {
440
418
  return;
441
419
  }
442
420
  subscription.lastPayload = payload;
@@ -462,45 +440,190 @@ export class WorkbenchWsHub {
462
440
  if (subscription.refreshController === controller) {
463
441
  subscription.refreshController = null;
464
442
  }
443
+ if (subscription.queuedRefresh && !subscription.refreshTimer) {
444
+ this.flushQueuedGitRefresh(client);
445
+ }
465
446
  });
466
447
  return subscription.refreshTask;
467
448
  }
468
- abortGitRefresh(client, reason) {
469
- this.clientGitSubscriptions.get(client)?.refreshController?.abort(new Error(reason));
449
+ ensureGitSubscription(client, workspaceId) {
450
+ const normalizedWorkspaceId = workspaceId.trim();
451
+ const current = this.clientGitSubscriptions.get(client);
452
+ if (current && current.workspaceId === normalizedWorkspaceId) {
453
+ return current;
454
+ }
455
+ if (current) {
456
+ current.refreshController?.abort(new Error("git subscription replaced"));
457
+ if (current.refreshTimer) {
458
+ clearTimeout(current.refreshTimer);
459
+ }
460
+ this.fileWatcher.unsubscribeGit(current.workspaceId);
461
+ }
462
+ const next = {
463
+ workspaceId: normalizedWorkspaceId,
464
+ lastPayload: null,
465
+ lastRequestedAt: 0,
466
+ refreshTask: null,
467
+ refreshController: null,
468
+ refreshTimer: null,
469
+ queuedRefresh: false,
470
+ queuedForce: false
471
+ };
472
+ this.clientGitSubscriptions.set(client, next);
473
+ this.fileWatcher.subscribeGit(normalizedWorkspaceId);
474
+ return next;
470
475
  }
471
- async refreshTerminalManagerSubscription(client, force = false) {
476
+ scheduleGitRefresh(client, options) {
477
+ const subscription = this.clientGitSubscriptions.get(client);
478
+ if (!subscription) {
479
+ return;
480
+ }
481
+ subscription.queuedRefresh = true;
482
+ subscription.queuedForce = subscription.queuedForce || (options?.force ?? false);
483
+ if (subscription.refreshTimer) {
484
+ clearTimeout(subscription.refreshTimer);
485
+ subscription.refreshTimer = null;
486
+ }
487
+ const quietWindowMs = options?.quietWindowMs ?? 0;
488
+ if (quietWindowMs > 0) {
489
+ this.deferQueuedGitRefresh(client, quietWindowMs);
490
+ return;
491
+ }
492
+ this.flushQueuedGitRefresh(client);
493
+ }
494
+ flushQueuedGitRefresh(client) {
495
+ const subscription = this.clientGitSubscriptions.get(client);
496
+ if (!subscription || subscription.refreshTimer || subscription.refreshTask || !subscription.queuedRefresh) {
497
+ return;
498
+ }
499
+ const force = subscription.queuedForce;
500
+ subscription.queuedRefresh = false;
501
+ subscription.queuedForce = false;
502
+ void this.refreshGitSubscription(client, force);
503
+ }
504
+ deferQueuedGitRefresh(client, delayMs) {
505
+ const subscription = this.clientGitSubscriptions.get(client);
506
+ if (!subscription) {
507
+ return;
508
+ }
509
+ if (subscription.refreshTimer) {
510
+ clearTimeout(subscription.refreshTimer);
511
+ }
512
+ subscription.refreshTimer = setTimeout(() => {
513
+ subscription.refreshTimer = null;
514
+ this.flushQueuedGitRefresh(client);
515
+ }, Math.max(0, delayMs));
516
+ }
517
+ ensureTerminalManagerSubscription(client, workspaceId) {
518
+ const normalizedWorkspaceId = workspaceId.trim();
519
+ const current = this.clientTerminalManagerSubscriptions.get(client);
520
+ if (current && current.workspaceId === normalizedWorkspaceId) {
521
+ return current;
522
+ }
523
+ if (current?.refreshTimer) {
524
+ clearTimeout(current.refreshTimer);
525
+ }
526
+ const next = {
527
+ workspaceId: normalizedWorkspaceId,
528
+ lastPayload: null,
529
+ refreshTask: null,
530
+ refreshTimer: null,
531
+ queuedRefresh: false,
532
+ queuedForce: false
533
+ };
534
+ this.clientTerminalManagerSubscriptions.set(client, next);
535
+ return next;
536
+ }
537
+ scheduleTerminalManagerRefresh(client, options) {
472
538
  const subscription = this.clientTerminalManagerSubscriptions.get(client);
473
539
  if (!subscription) {
474
540
  return;
475
541
  }
476
- try {
477
- const startedAtMs = terminalDebugNowMs();
478
- const snapshot = await this.workspacePanelSnapshotService.getTerminalManagerSnapshot(subscription.workspaceId, { force });
479
- const payloadStartedAtMs = terminalDebugNowMs();
480
- const payload = buildTerminalManagerPayload(snapshot);
481
- const payloadBuildMs = terminalDebugNowMs() - payloadStartedAtMs;
482
- if (payload === subscription.lastPayload) {
483
- return;
484
- }
485
- subscription.lastPayload = payload;
486
- const sendStartedAtMs = terminalDebugNowMs();
487
- client.send(payload);
488
- logTerminalDebug("workbench.terminal_manager_refresh.completed", {
489
- workspaceId: subscription.workspaceId,
490
- force,
491
- terminalCount: snapshot.terminals.length,
492
- templateCount: snapshot.templates.length,
493
- templateStatusCount: snapshot.templateStatuses.length,
494
- payloadBuildMs,
495
- sendMs: terminalDebugNowMs() - sendStartedAtMs,
496
- durationMs: terminalDebugNowMs() - startedAtMs
497
- });
542
+ subscription.queuedRefresh = true;
543
+ subscription.queuedForce = subscription.queuedForce || (options?.force ?? false);
544
+ if (subscription.refreshTimer) {
545
+ clearTimeout(subscription.refreshTimer);
546
+ subscription.refreshTimer = null;
498
547
  }
499
- catch (error) {
500
- this.reportAsyncError("refreshTerminalManagerSubscription", error, {
501
- workspaceId: subscription.workspaceId
502
- });
548
+ const quietWindowMs = options?.quietWindowMs ?? 0;
549
+ if (quietWindowMs > 0) {
550
+ this.deferQueuedTerminalManagerRefresh(client, quietWindowMs);
551
+ return;
552
+ }
553
+ this.flushQueuedTerminalManagerRefresh(client);
554
+ }
555
+ flushQueuedTerminalManagerRefresh(client) {
556
+ const subscription = this.clientTerminalManagerSubscriptions.get(client);
557
+ if (!subscription || subscription.refreshTimer || !subscription.queuedRefresh) {
558
+ return;
559
+ }
560
+ if (subscription.refreshTask) {
561
+ return;
562
+ }
563
+ const force = subscription.queuedForce;
564
+ subscription.queuedRefresh = false;
565
+ subscription.queuedForce = false;
566
+ void this.refreshTerminalManagerSubscription(client, force);
567
+ }
568
+ deferQueuedTerminalManagerRefresh(client, delayMs) {
569
+ const subscription = this.clientTerminalManagerSubscriptions.get(client);
570
+ if (!subscription) {
571
+ return;
572
+ }
573
+ if (subscription.refreshTimer) {
574
+ clearTimeout(subscription.refreshTimer);
575
+ }
576
+ subscription.refreshTimer = setTimeout(() => {
577
+ subscription.refreshTimer = null;
578
+ this.flushQueuedTerminalManagerRefresh(client);
579
+ }, Math.max(0, delayMs));
580
+ }
581
+ async refreshTerminalManagerSubscription(client, force = false) {
582
+ const subscription = this.clientTerminalManagerSubscriptions.get(client);
583
+ if (!subscription) {
584
+ return;
585
+ }
586
+ if (subscription.refreshTask) {
587
+ subscription.queuedRefresh = true;
588
+ subscription.queuedForce = subscription.queuedForce || force;
589
+ return subscription.refreshTask;
503
590
  }
591
+ subscription.refreshTask = (async () => {
592
+ try {
593
+ const startedAtMs = terminalDebugNowMs();
594
+ const snapshot = await this.workspacePanelSnapshotService.getTerminalManagerSnapshot(subscription.workspaceId, { force });
595
+ const payloadStartedAtMs = terminalDebugNowMs();
596
+ const payload = buildTerminalManagerPayload(snapshot);
597
+ const payloadBuildMs = terminalDebugNowMs() - payloadStartedAtMs;
598
+ if (payload === subscription.lastPayload) {
599
+ return;
600
+ }
601
+ subscription.lastPayload = payload;
602
+ const sendStartedAtMs = terminalDebugNowMs();
603
+ client.send(payload);
604
+ logTerminalDebug("workbench.terminal_manager_refresh.completed", {
605
+ workspaceId: subscription.workspaceId,
606
+ force,
607
+ terminalCount: snapshot.terminals.length,
608
+ templateCount: snapshot.templates.length,
609
+ templateStatusCount: snapshot.templateStatuses.length,
610
+ payloadBuildMs,
611
+ sendMs: terminalDebugNowMs() - sendStartedAtMs,
612
+ durationMs: terminalDebugNowMs() - startedAtMs
613
+ });
614
+ }
615
+ catch (error) {
616
+ this.reportAsyncError("refreshTerminalManagerSubscription", error, {
617
+ workspaceId: subscription.workspaceId
618
+ });
619
+ }
620
+ })().finally(() => {
621
+ subscription.refreshTask = null;
622
+ if (subscription.queuedRefresh && !subscription.refreshTimer) {
623
+ this.flushQueuedTerminalManagerRefresh(client);
624
+ }
625
+ });
626
+ return subscription.refreshTask;
504
627
  }
505
628
  async refreshWorkspaceManagementSubscription(client, force = false) {
506
629
  const subscription = this.clientWorkspaceManagementSubscriptions.get(client);
@@ -522,6 +645,13 @@ export class WorkbenchWsHub {
522
645
  });
523
646
  }
524
647
  }
648
+ async refreshWorkspaceManagementSubscriptions(userId) {
649
+ const channel = this.userChannels.get(userId);
650
+ if (!channel) {
651
+ return;
652
+ }
653
+ await Promise.allSettled([...channel.clients].map((client) => this.refreshWorkspaceManagementSubscription(client)));
654
+ }
525
655
  reportAsyncError(scope, error, context = {}) {
526
656
  const appError = error instanceof AppError
527
657
  ? error
@@ -604,6 +734,12 @@ function normalizePanelPaths(paths) {
604
734
  }
605
735
  return [...uniquePaths];
606
736
  }
737
+ function areStringArraysEqual(left, right) {
738
+ if (left.length !== right.length) {
739
+ return false;
740
+ }
741
+ return left.every((value, index) => value === right[index]);
742
+ }
607
743
  function buildWorkbenchPayload(snapshot) {
608
744
  return JSON.stringify({
609
745
  type: "workbench.snapshot",