@aiworker/web 1.23.0 → 1.23.1
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/dist/assets/{CodeMirrorCommentWidgets-B1Gizi3a.js → CodeMirrorCommentWidgets-teB3HmBT.js} +1 -1
- package/dist/assets/DiffView-DC9RX3MF.js +4 -0
- package/dist/assets/{DirtyBranchSwitchDialog-aig6zJjg.js → DirtyBranchSwitchDialog-DwPkzXsS.js} +1 -1
- package/dist/assets/FilesView-BFZkuu4O.js +13 -0
- package/dist/assets/{GitView-BXsS3jG6.js → GitView-B_x52MHY.js} +4 -4
- package/dist/assets/{JsonTreeView-D_hpXBnF.js → JsonTreeView-cmAzUTiw.js} +1 -1
- package/dist/assets/{LinearIssuesView-BoUiLQG1.js → LinearIssuesView-Dehh5afm.js} +1 -1
- package/dist/assets/{MarkdownImageGallery-B_B_a9KS.js → MarkdownImageGallery-DgNOqqhE.js} +1 -1
- package/dist/assets/{MarkdownRendererImpl-xgyyLiBM.js → MarkdownRendererImpl-DusmMZ_p.js} +2 -2
- package/dist/assets/{OnboardingScreen-DIp84OWt.js → OnboardingScreen-B8fm1H4m.js} +1 -1
- package/dist/assets/{PatchDiff-8p6nLJaU.js → PatchDiff-BVr7gJU0.js} +1 -1
- package/dist/assets/{PierreDiffViewer-DjC8nTk9.js → PierreDiffViewer-DzLt-nel.js} +1 -1
- package/dist/assets/PlanView-BmQUYT_z.js +5 -0
- package/dist/assets/SettingsView-ChWmAcZT.js +28 -0
- package/dist/assets/{SettingsWindow-Cpb4hM6q.js → SettingsWindow-Dy3M6ZB9.js} +1 -1
- package/dist/assets/{ToolOutputDialog-DfaKZXNU.js → ToolOutputDialog-cTl_eLIH.js} +6 -6
- package/dist/assets/{ToolPartDiffPreview-DtZLzrSK.js → ToolPartDiffPreview-C_3iYNJQ.js} +2 -2
- package/dist/assets/{VirtualizedFileDiff-C8i9HpTB.js → VirtualizedFileDiff-CAWthkoy.js} +4 -4
- package/dist/assets/WalkthroughView-C7AHOpCA.js +4 -0
- package/dist/assets/{computeVirtualFileMetrics-BBI7ZwrC.js → computeVirtualFileMetrics-BkiDSlTW.js} +2 -2
- package/dist/assets/de-DJsQr-p7.js +10 -0
- package/dist/assets/es-BuYMlA9g.js +16 -0
- package/dist/assets/fr-ePDay33C.js +14 -0
- package/dist/assets/index-BD8ILbxF.js +40 -0
- package/dist/assets/{index-jXSK5RrB.js → index-n6O3hklM.js} +1 -1
- package/dist/assets/index-zhgg-SDz.css +1 -0
- package/dist/assets/{isDiffPlainText-B_x_rVon.js → isDiffPlainText-GMdPz-71.js} +1 -1
- package/dist/assets/{isFilePlainText-DW_xMer1.js → isFilePlainText-BVuH1zH9.js} +1 -1
- package/dist/assets/ja-BzR9xOY_.js +16 -0
- package/dist/assets/{ko-idhXGF8j.js → ko-BEjHAm14.js} +3 -3
- package/dist/assets/main-BydQ1tr_.js +930 -0
- package/dist/assets/{main-CzdPuL0s.js → main-D3ZDQglc.js} +2 -2
- package/dist/assets/{markdownCore-Cn4Tyejo.js → markdownCore-Cb2MdZnV.js} +3 -3
- package/dist/assets/{miniChat-CCkqKOLp.js → miniChat-d01k801U.js} +2 -2
- package/dist/assets/{mobile-mvxTo055.js → mobile-D7VV-Ufz.js} +2 -2
- package/dist/assets/patchFileDiff-CdztVMz0.js +13 -0
- package/dist/assets/{pl-CGZWUqZJ.js → pl-rQmpxNIF.js} +3 -3
- package/dist/assets/pt-BR-CorbsSW-.js +16 -0
- package/dist/assets/renderElectronMiniChatApp-BGG9UIlh.js +2 -0
- package/dist/assets/renderMobileApp-BRP3J1iv.js +25 -0
- package/dist/assets/runtimeEndpointReset-BhT_-c52.js +41 -0
- package/dist/assets/scheduledTasksApi-BEuUF_cj.js +1 -0
- package/dist/assets/{shikiHighlight-CRl0SUC9.js → shikiHighlight-BzXUjlby.js} +1 -1
- package/dist/assets/surface-COKb77Ua.js +1 -0
- package/dist/assets/tr-CoAMXPtI.js +7 -0
- package/dist/assets/uk-YlAX0rRU.js +16 -0
- package/dist/assets/{useAppFontEffects-CPKeOpUo.js → useAppFontEffects-Cwyhjfl5.js} +153 -155
- package/dist/assets/useGitComparison-BCZVefdE.js +1 -0
- package/dist/assets/{useInlineCommentController-BiWz4AVb.js → useInlineCommentController-DDkpBEU8.js} +2 -2
- package/dist/assets/{useRootScrollLock-BS4-kr7P.js → useRootScrollLock-CwjoWuC4.js} +1 -1
- package/dist/assets/{vendor-base-ui-DdV8vhHn.js → vendor-base-ui-BY_5qla_.js} +1 -1
- package/dist/assets/{vendor-codemirror-language-data-Bg4AEeCr.js → vendor-codemirror-language-data-Cb1YnICj.js} +2 -2
- package/dist/assets/vendor-legendapp-list-BuBPBC3R.js +1 -0
- package/dist/assets/{vendor-zod-BPAANqvH.js → vendor-zod-B6551dwG.js} +2 -2
- package/dist/assets/{web-components-z7hfnS8n.js → web-components-LV3vjTxy.js} +1 -1
- package/dist/assets/zh-CN-DDYg0Jl7.js +16 -0
- package/dist/assets/zh-TW-CQKaCayr.js +16 -0
- package/dist/index.html +4 -4
- package/dist/mini-chat.html +4 -4
- package/dist/mobile.html +4 -4
- package/package.json +1 -1
- package/server/index.js +3 -13
- package/server/lib/agent-memory/runtime.js +7 -2
- package/server/lib/agent-memory/runtime.test.js +22 -0
- package/server/lib/event-stream/DOCUMENTATION.md +3 -0
- package/server/lib/event-stream/global-hub.js +29 -4
- package/server/lib/event-stream/global-hub.test.js +22 -0
- package/server/lib/event-stream/global-ws-bridge.js +15 -18
- package/server/lib/event-stream/protocol.js +21 -7
- package/server/lib/event-stream/runtime.js +9 -6
- package/server/lib/event-stream/runtime.test.js +60 -0
- package/server/lib/git/DOCUMENTATION.md +10 -1
- package/server/lib/git/routes.js +21 -8
- package/server/lib/git/routes.test.js +78 -0
- package/server/lib/git/service.js +159 -4
- package/server/lib/git/service.test.js +74 -0
- package/server/lib/github/DOCUMENTATION.md +5 -0
- package/server/lib/github/routes.js +2 -2
- package/server/lib/message-queue/DOCUMENTATION.md +7 -0
- package/server/lib/message-queue/runtime.js +13 -0
- package/server/lib/message-queue/runtime.test.js +31 -1
- package/server/lib/notifications/DOCUMENTATION.md +1 -1
- package/server/lib/notifications/emitter-runtime.js +4 -3
- package/server/lib/opencode/DOCUMENTATION.md +18 -4
- package/server/lib/opencode/env-runtime.js +5 -1
- package/server/lib/opencode/env-runtime.test.js +72 -0
- package/server/lib/opencode/feature-routes-runtime.js +15 -1
- package/server/lib/opencode/lifecycle.js +4 -3
- package/server/lib/opencode/lifecycle.test.js +29 -0
- package/server/lib/opencode/managed-plugin-config.js +1 -1
- package/server/lib/opencode/proxy.js +33 -0
- package/server/lib/opencode/settings-helpers.js +3 -0
- package/server/lib/opencode/settings-helpers.test.js +11 -1
- package/server/lib/opencode/settings-registry.json +3 -0
- package/server/lib/opencode/settings-runtime.js +36 -9
- package/server/lib/opencode/settings-runtime.test.js +158 -2
- package/server/lib/project-context/DOCUMENTATION.md +13 -3
- package/server/lib/project-context/runtime.js +8 -2
- package/server/lib/project-context/runtime.test.js +54 -0
- package/server/lib/projects/DOCUMENTATION.md +2 -1
- package/server/lib/projects/project-config.js +38 -9
- package/server/lib/projects/project-config.test.js +97 -0
- package/server/lib/projects/project-id.js +26 -0
- package/server/lib/relay/DOCUMENTATION.md +20 -0
- package/server/lib/relay/host-client.js +7 -0
- package/server/lib/relay/host-client.test.js +14 -0
- package/server/lib/scheduled-tasks/DOCUMENTATION.md +9 -1
- package/server/lib/scheduled-tasks/issue-2710-double-execution.test.js +28 -0
- package/server/lib/scheduled-tasks/runtime.js +12 -2
- package/server/lib/scheduled-tasks/runtime.test.js +53 -0
- package/server/lib/session-assist/DOCUMENTATION.md +131 -66
- package/server/lib/session-assist/context.js +127 -0
- package/server/lib/session-assist/context.test.js +132 -0
- package/server/lib/session-assist/prompt.js +54 -0
- package/server/lib/session-assist/runtime.js +111 -257
- package/server/lib/session-assist/runtime.test.js +203 -0
- package/server/lib/walkthrough/DOCUMENTATION.md +15 -5
- package/server/lib/walkthrough/pr-source.test.js +21 -0
- package/server/lib/walkthrough/pull-request.js +7 -4
- package/server/lib/walkthrough/pull-request.test.js +16 -0
- package/server/lib/walkthrough/routes.js +19 -0
- package/server/lib/walkthrough/routes.test.js +26 -0
- package/server/lib/walkthrough/sources.js +13 -2
- package/server/opencode-worktree-readiness.test.js +130 -0
- package/dist/assets/DiffView-DXjVsEOw.js +0 -4
- package/dist/assets/FilesView-CJdG0-wk.js +0 -17
- package/dist/assets/PlanView-CJTmOF6I.js +0 -5
- package/dist/assets/SettingsView-DD-JqIJx.js +0 -28
- package/dist/assets/WalkthroughView-DeKMvZlL.js +0 -4
- package/dist/assets/de-DCrsQ5zS.js +0 -10
- package/dist/assets/es-Dwv4YfH6.js +0 -16
- package/dist/assets/fr-BCWg4Cnx.js +0 -14
- package/dist/assets/index-B68h5S5B.css +0 -1
- package/dist/assets/index-DVwMarHI.js +0 -39
- package/dist/assets/ja-Bruw3RYC.js +0 -16
- package/dist/assets/main-BkyaQhJX.js +0 -930
- package/dist/assets/patchFileDiff-Csk4iNUi.js +0 -13
- package/dist/assets/pt-BR-B8V7u7tA.js +0 -16
- package/dist/assets/renderElectronMiniChatApp-BxoyujA5.js +0 -2
- package/dist/assets/renderMobileApp-B48CsP4r.js +0 -25
- package/dist/assets/runtimeEndpointReset-Bg5gx18H.js +0 -17
- package/dist/assets/scheduledTasksApi-BW0Etzwz.js +0 -1
- package/dist/assets/surface-oMvhsog3.js +0 -1
- package/dist/assets/tr-BK_KiO2V.js +0 -7
- package/dist/assets/uk-CG74xV8N.js +0 -16
- package/dist/assets/useGitComparison-DH220WmO.js +0 -1
- package/dist/assets/vendor-legendapp-list-gg7YLRW7.js +0 -1
- package/dist/assets/zh-CN-T4AXofZI.js +0 -16
- package/dist/assets/zh-TW-Dte40sxp.js +0 -16
- package/server/lib/mcp-reconnect/DOCUMENTATION.md +0 -55
- package/server/lib/mcp-reconnect/runtime.js +0 -122
- package/server/lib/mcp-reconnect/runtime.test.js +0 -140
|
@@ -5,6 +5,9 @@ const gitLibraries = {
|
|
|
5
5
|
unstageFiles: vi.fn(),
|
|
6
6
|
isGitRepository: vi.fn(),
|
|
7
7
|
getStatus: vi.fn(),
|
|
8
|
+
getWorktrees: vi.fn(),
|
|
9
|
+
observeWorktreeTopology: vi.fn(),
|
|
10
|
+
subscribeWorktreeTopologyChanges: vi.fn(),
|
|
8
11
|
};
|
|
9
12
|
|
|
10
13
|
vi.mock('./index.js', () => ({
|
|
@@ -12,6 +15,9 @@ vi.mock('./index.js', () => ({
|
|
|
12
15
|
unstageFiles: gitLibraries.unstageFiles,
|
|
13
16
|
isGitRepository: gitLibraries.isGitRepository,
|
|
14
17
|
getStatus: gitLibraries.getStatus,
|
|
18
|
+
getWorktrees: gitLibraries.getWorktrees,
|
|
19
|
+
observeWorktreeTopology: gitLibraries.observeWorktreeTopology,
|
|
20
|
+
subscribeWorktreeTopologyChanges: gitLibraries.subscribeWorktreeTopologyChanges,
|
|
15
21
|
}));
|
|
16
22
|
|
|
17
23
|
const { registerGitRoutes } = await import('./routes.js');
|
|
@@ -141,6 +147,78 @@ describe('git routes index mutations', () => {
|
|
|
141
147
|
});
|
|
142
148
|
});
|
|
143
149
|
|
|
150
|
+
describe('git worktree topology routes', () => {
|
|
151
|
+
beforeEach(() => {
|
|
152
|
+
gitLibraries.isGitRepository.mockReset();
|
|
153
|
+
gitLibraries.getStatus.mockReset();
|
|
154
|
+
gitLibraries.getWorktrees.mockReset();
|
|
155
|
+
gitLibraries.observeWorktreeTopology.mockReset();
|
|
156
|
+
gitLibraries.subscribeWorktreeTopologyChanges.mockReset();
|
|
157
|
+
gitLibraries.observeWorktreeTopology.mockResolvedValue(undefined);
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
it('observes the repository topology while serving status, never for non-repositories', async () => {
|
|
161
|
+
gitLibraries.isGitRepository.mockResolvedValueOnce(true).mockResolvedValueOnce(false);
|
|
162
|
+
gitLibraries.getStatus.mockResolvedValue({ current: 'main' });
|
|
163
|
+
const { app, getRoute } = createRouteRegistry();
|
|
164
|
+
registerGitRoutes(app);
|
|
165
|
+
const route = getRoute('GET', '/api/git/status');
|
|
166
|
+
|
|
167
|
+
const repoResponse = createMockResponse();
|
|
168
|
+
await route({ query: { directory: '/repo' } }, repoResponse);
|
|
169
|
+
expect(repoResponse.body).toEqual({ current: 'main' });
|
|
170
|
+
expect(gitLibraries.observeWorktreeTopology).toHaveBeenCalledWith('/repo');
|
|
171
|
+
|
|
172
|
+
await route({ query: { directory: '/plain-folder' } }, createMockResponse());
|
|
173
|
+
expect(gitLibraries.observeWorktreeTopology).toHaveBeenCalledTimes(1);
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
it('observes topology after a repository listing and reports listing failures', async () => {
|
|
177
|
+
const errorSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
|
|
178
|
+
gitLibraries.getWorktrees
|
|
179
|
+
.mockResolvedValueOnce([{ path: '/repo', branch: 'main' }])
|
|
180
|
+
.mockResolvedValueOnce([])
|
|
181
|
+
.mockRejectedValueOnce(new Error('git failed'));
|
|
182
|
+
const { app, getRoute } = createRouteRegistry();
|
|
183
|
+
registerGitRoutes(app);
|
|
184
|
+
const route = getRoute('GET', '/api/git/worktrees');
|
|
185
|
+
|
|
186
|
+
const listed = createMockResponse();
|
|
187
|
+
await route({ query: { directory: '/repo' } }, listed);
|
|
188
|
+
expect(listed.body).toEqual([{ path: '/repo', branch: 'main' }]);
|
|
189
|
+
expect(gitLibraries.observeWorktreeTopology).toHaveBeenCalledWith('/repo');
|
|
190
|
+
|
|
191
|
+
await route({ query: { directory: '/plain-folder' } }, createMockResponse());
|
|
192
|
+
expect(gitLibraries.observeWorktreeTopology).toHaveBeenCalledTimes(1);
|
|
193
|
+
|
|
194
|
+
const failed = createMockResponse();
|
|
195
|
+
await route({ query: { directory: '/repo' } }, failed);
|
|
196
|
+
expect(failed.statusCode).toBe(500);
|
|
197
|
+
expect(failed.body).toEqual({ error: 'git failed' });
|
|
198
|
+
errorSpy.mockRestore();
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
it('forwards topology changes to the control event emitter once', async () => {
|
|
202
|
+
let listener = null;
|
|
203
|
+
gitLibraries.subscribeWorktreeTopologyChanges.mockImplementation((next) => {
|
|
204
|
+
listener = next;
|
|
205
|
+
return () => undefined;
|
|
206
|
+
});
|
|
207
|
+
gitLibraries.getWorktrees.mockResolvedValue([]);
|
|
208
|
+
const emitWorktreeChanged = vi.fn();
|
|
209
|
+
const { app, getRoute } = createRouteRegistry();
|
|
210
|
+
registerGitRoutes(app, { emitWorktreeChanged });
|
|
211
|
+
const route = getRoute('GET', '/api/git/worktrees');
|
|
212
|
+
|
|
213
|
+
await route({ query: { directory: '/repo' } }, createMockResponse());
|
|
214
|
+
await route({ query: { directory: '/repo' } }, createMockResponse());
|
|
215
|
+
expect(gitLibraries.subscribeWorktreeTopologyChanges).toHaveBeenCalledTimes(1);
|
|
216
|
+
|
|
217
|
+
listener({ directories: ['/repo'], at: 123 });
|
|
218
|
+
expect(emitWorktreeChanged).toHaveBeenCalledWith({ directories: ['/repo'], at: 123 });
|
|
219
|
+
});
|
|
220
|
+
});
|
|
221
|
+
|
|
144
222
|
describe('git routes status discovery', () => {
|
|
145
223
|
beforeEach(() => {
|
|
146
224
|
gitLibraries.isGitRepository.mockReset();
|
|
@@ -4064,14 +4064,167 @@ export async function getWorktrees(directory) {
|
|
|
4064
4064
|
// OpenCode's working directory or an unconfigured project path), git
|
|
4065
4065
|
// exits with "fatal: not a git repository ...". Treat that as an
|
|
4066
4066
|
// authoritative empty result so the route handler can still respond
|
|
4067
|
-
// 200 [] and the desktop main.log stays free of noise.
|
|
4068
|
-
|
|
4069
|
-
|
|
4067
|
+
// 200 [] and the desktop main.log stays free of noise. Any other failure
|
|
4068
|
+
// is a failure: callers keep their last known topology instead of
|
|
4069
|
+
// treating "git could not answer" as "there are no worktrees".
|
|
4070
|
+
if (isNotGitRepositoryError(error)) return [];
|
|
4071
|
+
throw error;
|
|
4072
|
+
}
|
|
4073
|
+
}
|
|
4074
|
+
|
|
4075
|
+
// ---------------------------------------------------------------------------
|
|
4076
|
+
// Worktree topology change tracking
|
|
4077
|
+
//
|
|
4078
|
+
// Linked worktrees are registered under the repository's common Git directory
|
|
4079
|
+
// (`<common>/worktrees/<name>`). Instead of watching the filesystem, the server
|
|
4080
|
+
// fingerprints that directory while handling requests clients already make
|
|
4081
|
+
// (status, worktree listing) and after its own worktree create/remove, and
|
|
4082
|
+
// tells connected clients when the set of worktrees changed. Cost scales with
|
|
4083
|
+
// user activity, never with the number of registered projects.
|
|
4084
|
+
// ---------------------------------------------------------------------------
|
|
4085
|
+
|
|
4086
|
+
const MAX_TRACKED_WORKTREE_DIRECTORIES = 500;
|
|
4087
|
+
const MAX_TRACKED_WORKTREE_REPOSITORIES = 200;
|
|
4088
|
+
const MAX_DIRECTORIES_PER_WORKTREE_REPOSITORY = 100;
|
|
4089
|
+
const worktreeTopologyListeners = new Set();
|
|
4090
|
+
const worktreeRepositoryKeyByDirectory = new Map();
|
|
4091
|
+
const worktreeTopologyByRepository = new Map();
|
|
4092
|
+
|
|
4093
|
+
export function subscribeWorktreeTopologyChanges(listener) {
|
|
4094
|
+
worktreeTopologyListeners.add(listener);
|
|
4095
|
+
return () => {
|
|
4096
|
+
worktreeTopologyListeners.delete(listener);
|
|
4097
|
+
};
|
|
4098
|
+
}
|
|
4099
|
+
|
|
4100
|
+
const rememberWorktreeRepositoryKey = (directoryPath, key) => {
|
|
4101
|
+
worktreeRepositoryKeyByDirectory.delete(directoryPath);
|
|
4102
|
+
worktreeRepositoryKeyByDirectory.set(directoryPath, key);
|
|
4103
|
+
while (worktreeRepositoryKeyByDirectory.size > MAX_TRACKED_WORKTREE_DIRECTORIES) {
|
|
4104
|
+
const oldest = worktreeRepositoryKeyByDirectory.keys().next().value;
|
|
4105
|
+
if (oldest === undefined) break;
|
|
4106
|
+
worktreeRepositoryKeyByDirectory.delete(oldest);
|
|
4107
|
+
}
|
|
4108
|
+
};
|
|
4109
|
+
|
|
4110
|
+
/**
|
|
4111
|
+
* Canonical common Git directory for `directoryPath`, resolved with git once per
|
|
4112
|
+
* directory and cached. Returns null when git cannot answer (not a repository,
|
|
4113
|
+
* missing directory).
|
|
4114
|
+
*/
|
|
4115
|
+
const resolveWorktreeRepositoryKey = async (directoryPath) => {
|
|
4116
|
+
const cached = worktreeRepositoryKeyByDirectory.get(directoryPath);
|
|
4117
|
+
if (cached) {
|
|
4118
|
+
rememberWorktreeRepositoryKey(directoryPath, cached);
|
|
4119
|
+
return cached;
|
|
4120
|
+
}
|
|
4121
|
+
const result = await runGitCommand(directoryPath, ['rev-parse', '--git-common-dir']);
|
|
4122
|
+
const rawCommonDir = String(result.stdout || '').trim();
|
|
4123
|
+
if (!result.success || !rawCommonDir) {
|
|
4124
|
+
return null;
|
|
4125
|
+
}
|
|
4126
|
+
const commonDir = path.resolve(directoryPath, rawCommonDir);
|
|
4127
|
+
let key = commonDir;
|
|
4128
|
+
try {
|
|
4129
|
+
key = fs.realpathSync(commonDir);
|
|
4130
|
+
} catch {
|
|
4131
|
+
// Keep the resolved path; a missing common dir cannot register worktrees.
|
|
4132
|
+
}
|
|
4133
|
+
rememberWorktreeRepositoryKey(directoryPath, key);
|
|
4134
|
+
return key;
|
|
4135
|
+
};
|
|
4136
|
+
|
|
4137
|
+
/**
|
|
4138
|
+
* Cheap identity of the registered linked-worktree set: the `worktrees`
|
|
4139
|
+
* directory's mtime plus its entry names. Adding, removing, or pruning a
|
|
4140
|
+
* worktree changes at least one of them; `git worktree move` rewrites files
|
|
4141
|
+
* inside an entry and is not detected.
|
|
4142
|
+
*/
|
|
4143
|
+
const readWorktreeTopologyFingerprint = (repositoryKey) => {
|
|
4144
|
+
const worktreesDir = path.join(repositoryKey, 'worktrees');
|
|
4145
|
+
try {
|
|
4146
|
+
const stat = fs.statSync(worktreesDir);
|
|
4147
|
+
const names = fs.readdirSync(worktreesDir).sort();
|
|
4148
|
+
return `${stat.mtimeMs}:${names.join('\0')}`;
|
|
4149
|
+
} catch {
|
|
4150
|
+
return 'none';
|
|
4151
|
+
}
|
|
4152
|
+
};
|
|
4153
|
+
|
|
4154
|
+
const trackWorktreeTopologyDirectory = (repositoryKey, directoryPath) => {
|
|
4155
|
+
let entry = worktreeTopologyByRepository.get(repositoryKey);
|
|
4156
|
+
if (!entry) {
|
|
4157
|
+
entry = { directories: new Set(), fingerprint: null };
|
|
4158
|
+
}
|
|
4159
|
+
// Re-insert so the map stays ordered by last use; the least recently used
|
|
4160
|
+
// repository is dropped first once the bound is reached.
|
|
4161
|
+
worktreeTopologyByRepository.delete(repositoryKey);
|
|
4162
|
+
worktreeTopologyByRepository.set(repositoryKey, entry);
|
|
4163
|
+
while (worktreeTopologyByRepository.size > MAX_TRACKED_WORKTREE_REPOSITORIES) {
|
|
4164
|
+
const oldest = worktreeTopologyByRepository.keys().next().value;
|
|
4165
|
+
if (oldest === undefined) break;
|
|
4166
|
+
worktreeTopologyByRepository.delete(oldest);
|
|
4167
|
+
}
|
|
4168
|
+
if (entry.directories.size < MAX_DIRECTORIES_PER_WORKTREE_REPOSITORY) {
|
|
4169
|
+
entry.directories.add(directoryPath);
|
|
4170
|
+
}
|
|
4171
|
+
return entry;
|
|
4172
|
+
};
|
|
4173
|
+
|
|
4174
|
+
const notifyWorktreeTopologyChanged = (entry) => {
|
|
4175
|
+
const event = { directories: [...entry.directories], at: Date.now() };
|
|
4176
|
+
for (const listener of worktreeTopologyListeners) {
|
|
4177
|
+
try {
|
|
4178
|
+
listener(event);
|
|
4179
|
+
} catch (error) {
|
|
4180
|
+
console.warn('Worktree topology listener failed:', error?.message || error);
|
|
4070
4181
|
}
|
|
4071
|
-
|
|
4182
|
+
}
|
|
4183
|
+
};
|
|
4184
|
+
|
|
4185
|
+
/**
|
|
4186
|
+
* Compare the repository's worktree set with the last one seen for it and
|
|
4187
|
+
* notify listeners when it changed. The first observation only records a
|
|
4188
|
+
* baseline. Called from request handlers that already touch the repository;
|
|
4189
|
+
* never throws.
|
|
4190
|
+
*/
|
|
4191
|
+
export async function observeWorktreeTopology(directory) {
|
|
4192
|
+
const directoryPath = normalizeDirectoryPath(directory);
|
|
4193
|
+
if (!directoryPath) return;
|
|
4194
|
+
try {
|
|
4195
|
+
const repositoryKey = await resolveWorktreeRepositoryKey(directoryPath);
|
|
4196
|
+
if (!repositoryKey) return;
|
|
4197
|
+
const entry = trackWorktreeTopologyDirectory(repositoryKey, directoryPath);
|
|
4198
|
+
const fingerprint = readWorktreeTopologyFingerprint(repositoryKey);
|
|
4199
|
+
if (entry.fingerprint === fingerprint) return;
|
|
4200
|
+
const hadBaseline = entry.fingerprint !== null;
|
|
4201
|
+
entry.fingerprint = fingerprint;
|
|
4202
|
+
if (hadBaseline) notifyWorktreeTopologyChanged(entry);
|
|
4203
|
+
} catch (error) {
|
|
4204
|
+
console.warn('Failed to observe worktree topology:', error?.message || error);
|
|
4072
4205
|
}
|
|
4073
4206
|
}
|
|
4074
4207
|
|
|
4208
|
+
/**
|
|
4209
|
+
* Record that this server changed the repository's worktree set itself and
|
|
4210
|
+
* notify listeners right away. `directory` is any directory inside the
|
|
4211
|
+
* repository; never throws so a notification problem cannot fail the
|
|
4212
|
+
* operation that triggered it.
|
|
4213
|
+
*/
|
|
4214
|
+
const publishWorktreeTopologyChange = async (directory) => {
|
|
4215
|
+
const directoryPath = normalizeDirectoryPath(directory);
|
|
4216
|
+
if (!directoryPath) return;
|
|
4217
|
+
try {
|
|
4218
|
+
const repositoryKey = await resolveWorktreeRepositoryKey(directoryPath);
|
|
4219
|
+
if (!repositoryKey) return;
|
|
4220
|
+
const entry = trackWorktreeTopologyDirectory(repositoryKey, directoryPath);
|
|
4221
|
+
entry.fingerprint = readWorktreeTopologyFingerprint(repositoryKey);
|
|
4222
|
+
notifyWorktreeTopologyChanged(entry);
|
|
4223
|
+
} catch (error) {
|
|
4224
|
+
console.warn('Failed to publish worktree topology change:', error?.message || error);
|
|
4225
|
+
}
|
|
4226
|
+
};
|
|
4227
|
+
|
|
4075
4228
|
export async function validateWorktreeCreate(directory, input = {}) {
|
|
4076
4229
|
const mode = input?.mode === 'existing' ? 'existing' : 'new';
|
|
4077
4230
|
const errors = [];
|
|
@@ -4329,6 +4482,7 @@ async function attachGitWorktreeToCandidate(context, candidate, input = {}) {
|
|
|
4329
4482
|
}
|
|
4330
4483
|
|
|
4331
4484
|
await runGitCommandOrThrow(context.primaryWorktree, worktreeAddArgs, 'Failed to create git worktree');
|
|
4485
|
+
await publishWorktreeTopologyChange(context.primaryWorktree);
|
|
4332
4486
|
|
|
4333
4487
|
const upstreamRemote = shouldSetUpstream
|
|
4334
4488
|
? String(input?.upstreamRemote || inferredUpstream?.remote || '').trim()
|
|
@@ -4551,6 +4705,7 @@ export async function removeWorktree(directory, input = {}) {
|
|
|
4551
4705
|
['worktree', 'remove', '--force', matchedEntry.worktree],
|
|
4552
4706
|
'Failed to remove git worktree'
|
|
4553
4707
|
);
|
|
4708
|
+
await publishWorktreeTopologyChange(context.primaryWorktree);
|
|
4554
4709
|
|
|
4555
4710
|
if (deleteLocalBranch) {
|
|
4556
4711
|
const branchName = cleanBranchName(String(matchedEntry.branchRef || matchedEntry.branch || '').trim());
|
|
@@ -23,6 +23,7 @@ import {
|
|
|
23
23
|
getStatus,
|
|
24
24
|
getWorktrees,
|
|
25
25
|
isGitRepository,
|
|
26
|
+
observeWorktreeTopology,
|
|
26
27
|
populateWorktreeWithLockRecovery,
|
|
27
28
|
removeWorktree,
|
|
28
29
|
resolvePrimaryWorktreeRoot,
|
|
@@ -32,6 +33,7 @@ import {
|
|
|
32
33
|
revertCommit,
|
|
33
34
|
setLocalIdentity,
|
|
34
35
|
stageFiles,
|
|
36
|
+
subscribeWorktreeTopologyChanges,
|
|
35
37
|
unstageFiles,
|
|
36
38
|
applyHunk,
|
|
37
39
|
getDiff,
|
|
@@ -680,6 +682,78 @@ describe('getWorktrees', () => {
|
|
|
680
682
|
expect(Array.isArray(result)).toBe(true);
|
|
681
683
|
expect(warnSpy).not.toHaveBeenCalled();
|
|
682
684
|
});
|
|
685
|
+
it('notifies subscribers only when another git process changes the worktree set', async () => {
|
|
686
|
+
if (!canRunGit()) return;
|
|
687
|
+
|
|
688
|
+
const repo = createTempDir();
|
|
689
|
+
runGit(repo, ['init', '-b', 'main']);
|
|
690
|
+
runGit(repo, ['config', 'user.email', 'test@example.com']);
|
|
691
|
+
runGit(repo, ['config', 'user.name', 'Test User']);
|
|
692
|
+
runGit(repo, ['commit', '--allow-empty', '-m', 'init']);
|
|
693
|
+
const worktreePath = path.join(createTempDir(), 'feature');
|
|
694
|
+
|
|
695
|
+
const events = [];
|
|
696
|
+
const unsubscribe = subscribeWorktreeTopologyChanges((event) => events.push(event));
|
|
697
|
+
try {
|
|
698
|
+
await observeWorktreeTopology(repo);
|
|
699
|
+
await observeWorktreeTopology(repo);
|
|
700
|
+
expect(events).toHaveLength(0);
|
|
701
|
+
|
|
702
|
+
runGit(repo, ['worktree', 'add', worktreePath, '-b', 'feature']);
|
|
703
|
+
await observeWorktreeTopology(worktreePath);
|
|
704
|
+
expect(events).toHaveLength(1);
|
|
705
|
+
expect(events[0].directories).toEqual(expect.arrayContaining([repo, worktreePath]));
|
|
706
|
+
|
|
707
|
+
await observeWorktreeTopology(repo);
|
|
708
|
+
expect(events).toHaveLength(1);
|
|
709
|
+
|
|
710
|
+
runGit(repo, ['worktree', 'remove', worktreePath]);
|
|
711
|
+
await observeWorktreeTopology(repo);
|
|
712
|
+
expect(events).toHaveLength(2);
|
|
713
|
+
} finally {
|
|
714
|
+
unsubscribe();
|
|
715
|
+
}
|
|
716
|
+
});
|
|
717
|
+
|
|
718
|
+
it('publishes worktrees this server creates and removes', async () => {
|
|
719
|
+
if (!canRunGit()) return;
|
|
720
|
+
|
|
721
|
+
const previousXdgDataHome = process.env.XDG_DATA_HOME;
|
|
722
|
+
process.env.XDG_DATA_HOME = createTempDir();
|
|
723
|
+
const events = [];
|
|
724
|
+
const unsubscribe = subscribeWorktreeTopologyChanges((event) => events.push(event));
|
|
725
|
+
try {
|
|
726
|
+
const repo = createTempDir();
|
|
727
|
+
runGit(repo, ['init', '-b', 'main']);
|
|
728
|
+
runGit(repo, ['config', 'user.email', 'test@example.com']);
|
|
729
|
+
runGit(repo, ['config', 'user.name', 'Test User']);
|
|
730
|
+
runGit(repo, ['commit', '--allow-empty', '-m', 'init']);
|
|
731
|
+
await observeWorktreeTopology(repo);
|
|
732
|
+
|
|
733
|
+
const created = await createWorktree(repo, {
|
|
734
|
+
mode: 'new',
|
|
735
|
+
worktreeName: 'published',
|
|
736
|
+
branchName: 'aiworker/published',
|
|
737
|
+
});
|
|
738
|
+
expect(events).toHaveLength(1);
|
|
739
|
+
expect(events[0].directories).toContain(repo);
|
|
740
|
+
|
|
741
|
+
// The publish refreshed the baseline, so the next observation is quiet.
|
|
742
|
+
await observeWorktreeTopology(repo);
|
|
743
|
+
expect(events).toHaveLength(1);
|
|
744
|
+
|
|
745
|
+
await removeWorktree(repo, { directory: created.path });
|
|
746
|
+
expect(events).toHaveLength(2);
|
|
747
|
+
} finally {
|
|
748
|
+
unsubscribe();
|
|
749
|
+
if (previousXdgDataHome === undefined) {
|
|
750
|
+
delete process.env.XDG_DATA_HOME;
|
|
751
|
+
} else {
|
|
752
|
+
process.env.XDG_DATA_HOME = previousXdgDataHome;
|
|
753
|
+
}
|
|
754
|
+
}
|
|
755
|
+
});
|
|
756
|
+
|
|
683
757
|
it('flags a worktree whose directory was deleted outside git as prunable', async () => {
|
|
684
758
|
const repo = createTempDir();
|
|
685
759
|
runGit(repo, ['init', '-b', 'main']);
|
|
@@ -63,6 +63,11 @@
|
|
|
63
63
|
|
|
64
64
|
## Consumers of PR data
|
|
65
65
|
|
|
66
|
+
PR list/search failures propagate as errors instead of successful empty lists.
|
|
67
|
+
Comparison pickers use these responses to offer retry rather than claiming the
|
|
68
|
+
repository has no PRs. A failed repository in a multi-repository listing fails
|
|
69
|
+
that page, so callers cannot mistake a partial page for a complete one.
|
|
70
|
+
|
|
66
71
|
- `packages/ui/src/components/session/SessionSidebar.tsx` reads all PR entries and maps them to `directory::branch`.
|
|
67
72
|
- `packages/ui/src/components/session/sidebar/SessionGroupSection.tsx` renders the compact badge, PR number, title, checks summary, and GitHub link.
|
|
68
73
|
- `packages/ui/src/components/views/git/PullRequestSection.tsx` uses the same shared entry for the full PR workflow.
|
|
@@ -1513,7 +1513,7 @@ export function registerGitHubRoutes(app) {
|
|
|
1513
1513
|
return res.json({ connected: true, repo, prs, page: effectivePage, hasMore });
|
|
1514
1514
|
} catch (error) {
|
|
1515
1515
|
console.error('Failed to search GitHub PRs:', error);
|
|
1516
|
-
|
|
1516
|
+
throw error;
|
|
1517
1517
|
}
|
|
1518
1518
|
}
|
|
1519
1519
|
|
|
@@ -1532,7 +1532,7 @@ export function registerGitHubRoutes(app) {
|
|
|
1532
1532
|
return { prs, hasMore };
|
|
1533
1533
|
} catch (error) {
|
|
1534
1534
|
console.warn(`Failed to list PRs for ${repoRef.owner}/${repoRef.repo}:`, error?.message || error);
|
|
1535
|
-
|
|
1535
|
+
throw error;
|
|
1536
1536
|
}
|
|
1537
1537
|
};
|
|
1538
1538
|
|
|
@@ -31,6 +31,7 @@ send never re-resolves mutable UI state:
|
|
|
31
31
|
{
|
|
32
32
|
id, createdAt,
|
|
33
33
|
content, // raw text for display and editing
|
|
34
|
+
contextPreview?, // bounded display-only summary captured by the UI
|
|
34
35
|
text, // text to deliver (agent mention stripped, file mentions resolved); defaults to content
|
|
35
36
|
agentMention?, // delivered as an `agent` part
|
|
36
37
|
attachments: [{ id, filename, mimeType, size, source, serverPath?, dataUrl }],
|
|
@@ -56,6 +57,12 @@ context entry). Public snapshots and broadcasts strip the payloads —
|
|
|
56
57
|
attachment `dataUrl` (megabytes of base64) and `context` (a PR diff, say) —
|
|
57
58
|
so they do not ride every update; the only way to get them back is a `take`.
|
|
58
59
|
|
|
60
|
+
Snapshots retain `contextPreview`, capped at 100 characters plus an ellipsis.
|
|
61
|
+
It carries the attached comment or context label when `content` is empty and
|
|
62
|
+
never replaces editable text or delivered parts. Older items without a summary
|
|
63
|
+
derive one from the attached comment metadata or the first non-instruction
|
|
64
|
+
context text. This optional field needs no queue-file migration.
|
|
65
|
+
|
|
59
66
|
## Persistence
|
|
60
67
|
|
|
61
68
|
`<data-dir>/message-queue.json` (`AIWORKER_DATA_DIR` or
|
|
@@ -133,6 +133,8 @@ export const parseQueuedItemInput = (value) => {
|
|
|
133
133
|
if (agentMention) item.agentMention = agentMention;
|
|
134
134
|
item.attachments = attachments;
|
|
135
135
|
item.context = context;
|
|
136
|
+
const contextPreview = asNonEmptyString(raw.contextPreview).slice(0, 103);
|
|
137
|
+
if (contextPreview) item.contextPreview = contextPreview;
|
|
136
138
|
item.sendConfig = sendConfig;
|
|
137
139
|
return item;
|
|
138
140
|
};
|
|
@@ -157,6 +159,17 @@ const toPublicItem = (item) => {
|
|
|
157
159
|
const publicItem = { id: item.id, createdAt: item.createdAt, content: item.content, text: item.text };
|
|
158
160
|
if (item.agentMention) publicItem.agentMention = item.agentMention;
|
|
159
161
|
publicItem.attachments = item.attachments.map(toPublicAttachment);
|
|
162
|
+
// Older persisted items have no UI summary. Prefer their attached comment
|
|
163
|
+
// before falling back to the model-facing context text.
|
|
164
|
+
const contextPreview = item.contextPreview || item.context
|
|
165
|
+
.filter((part) => part.kind !== 'instruction')
|
|
166
|
+
.map((part) => asNonEmptyString(asRecord(part.metadata?.aiworkerContext)?.text) || part.text.trim())
|
|
167
|
+
.find(Boolean);
|
|
168
|
+
if (contextPreview) {
|
|
169
|
+
const firstLine = contextPreview.split('\n', 1)[0];
|
|
170
|
+
publicItem.contextPreview = firstLine.slice(0, 100)
|
|
171
|
+
+ (contextPreview.length > firstLine.length || firstLine.length > 100 ? '...' : '');
|
|
172
|
+
}
|
|
160
173
|
publicItem.sendConfig = { ...item.sendConfig };
|
|
161
174
|
return publicItem;
|
|
162
175
|
};
|
|
@@ -251,7 +251,7 @@ describe('message queue runtime', () => {
|
|
|
251
251
|
const first = createRuntime({ dataDir });
|
|
252
252
|
first.runtime.start();
|
|
253
253
|
first.openCode.state.statuses = { [SESSION]: { type: 'busy' } };
|
|
254
|
-
await first.runtime.enqueue(SESSION, DIRECTORY, item({ content: 'persisted', text: 'persisted' }));
|
|
254
|
+
await first.runtime.enqueue(SESSION, DIRECTORY, item({ content: 'persisted', text: 'persisted', contextPreview: 'Saved context preview' }));
|
|
255
255
|
await first.runtime.flush();
|
|
256
256
|
first.runtime.stop();
|
|
257
257
|
|
|
@@ -259,6 +259,7 @@ describe('message queue runtime', () => {
|
|
|
259
259
|
second.runtime.start();
|
|
260
260
|
await second.runtime.load();
|
|
261
261
|
expect(second.runtime.sessionSnapshot(SESSION).items.map((entry) => entry.content)).toEqual(['persisted']);
|
|
262
|
+
expect(second.runtime.sessionSnapshot(SESSION).items[0].contextPreview).toBe('Saved context preview');
|
|
262
263
|
second.connect();
|
|
263
264
|
await settle();
|
|
264
265
|
expect(second.openCode.state.sent).toHaveLength(1);
|
|
@@ -315,6 +316,35 @@ describe('message queue runtime', () => {
|
|
|
315
316
|
expect(runtime.snapshot().sessions).toEqual([]);
|
|
316
317
|
});
|
|
317
318
|
|
|
319
|
+
it('retains a bounded context preview in snapshots and broadcasts without exposing the full payload', async () => {
|
|
320
|
+
const { runtime, broadcasts } = createRuntime();
|
|
321
|
+
runtime.start();
|
|
322
|
+
const context = [{ kind: 'context', text: 'Full quoted content', metadata: { aiworkerContext: { kind: 'chat-quote', quote: 'Original answer', text: 'Explain this' } } }];
|
|
323
|
+
const { itemId } = await runtime.enqueue(SESSION, DIRECTORY, item({ content: '', text: '', context, contextPreview: 'Explain this' }));
|
|
324
|
+
const projected = runtime.sessionSnapshot(SESSION).items[0];
|
|
325
|
+
expect(projected.contextPreview).toBe('Explain this');
|
|
326
|
+
expect(projected.content).toBe('');
|
|
327
|
+
expect(projected.text).toBe('');
|
|
328
|
+
expect(projected).not.toHaveProperty('context');
|
|
329
|
+
expect(broadcasts.at(-1).properties.session.items[0].contextPreview).toBe('Explain this');
|
|
330
|
+
const taken = await runtime.take(SESSION, itemId);
|
|
331
|
+
expect(taken.item.context).toEqual(context);
|
|
332
|
+
expect(taken.item.content).toBe('');
|
|
333
|
+
|
|
334
|
+
await runtime.enqueue(SESSION, DIRECTORY, item({ content: '', text: '', context, contextPreview: 'a'.repeat(5000) }));
|
|
335
|
+
expect(runtime.sessionSnapshot(SESSION).items[0].contextPreview).toBe('a'.repeat(100) + '...');
|
|
336
|
+
});
|
|
337
|
+
|
|
338
|
+
it('derives a preview for older queued annotations without a saved summary', async () => {
|
|
339
|
+
const { runtime } = createRuntime();
|
|
340
|
+
runtime.start();
|
|
341
|
+
await runtime.enqueue(SESSION, DIRECTORY, item({ content: '', text: '', context: [
|
|
342
|
+
{ kind: 'instruction', text: 'Use the skill' },
|
|
343
|
+
{ kind: 'context', text: 'Model-facing wrapper', metadata: { aiworkerContext: { kind: 'browser-annotation', text: 'Fix the button\nMore detail' } } },
|
|
344
|
+
] }));
|
|
345
|
+
expect(runtime.sessionSnapshot(SESSION).items[0].contextPreview).toBe('Fix the button...');
|
|
346
|
+
});
|
|
347
|
+
|
|
318
348
|
it('names the directory in the broadcast that empties a queue', async () => {
|
|
319
349
|
// The UI keys its projection by directory; without it the client cannot
|
|
320
350
|
// tell which queue just delivered its last message and keeps showing it.
|
|
@@ -77,7 +77,7 @@ This module provides notification message preparation utilities for the web serv
|
|
|
77
77
|
### Emitter runtime API (emitter-runtime.js)
|
|
78
78
|
- `createNotificationEmitterRuntime(dependencies)`: creates runtime for unified notification emission channels.
|
|
79
79
|
- Returned API:
|
|
80
|
-
|
|
80
|
+
- `writeSseEvent(res, payload, serializedPayload?)`, where broadcast callers may share one JSON encoding across recipients
|
|
81
81
|
- `emitDesktopNotification(payload)`
|
|
82
82
|
- `broadcastUiNotification(payload)`
|
|
83
83
|
|
|
@@ -22,8 +22,8 @@ export const createNotificationEmitterRuntime = (dependencies) => {
|
|
|
22
22
|
onDesktopNotification = typeof cb === 'function' ? cb : null;
|
|
23
23
|
};
|
|
24
24
|
|
|
25
|
-
const writeSseEvent = (res, payload) => {
|
|
26
|
-
res.write(`data: ${
|
|
25
|
+
const writeSseEvent = (res, payload, serializedPayload = JSON.stringify(payload)) => {
|
|
26
|
+
res.write(`data: ${serializedPayload}\n\n`);
|
|
27
27
|
};
|
|
28
28
|
|
|
29
29
|
const emitDesktopNotification = (payload) => {
|
|
@@ -91,9 +91,10 @@ export const createNotificationEmitterRuntime = (dependencies) => {
|
|
|
91
91
|
return;
|
|
92
92
|
}
|
|
93
93
|
|
|
94
|
+
const serializedPayload = JSON.stringify(syntheticPayload);
|
|
94
95
|
for (const res of clients) {
|
|
95
96
|
try {
|
|
96
|
-
writeSseEvent(res, syntheticPayload);
|
|
97
|
+
writeSseEvent(res, syntheticPayload, serializedPayload);
|
|
97
98
|
} catch {
|
|
98
99
|
// ignore
|
|
99
100
|
}
|
|
@@ -34,8 +34,7 @@ This module provides OpenCode server integration utilities for the web server ru
|
|
|
34
34
|
- `packages/web/server/lib/opencode/startup-performance.js`: opt-in startup phase diagnostics with fixed labels and numeric metadata allowlists.
|
|
35
35
|
- `packages/web/server/lib/agent-tool/runtime.js`: managed OpenCode custom-tool materialization, environment injection, loopback authentication, and fixed CLI action dispatch.
|
|
36
36
|
- `packages/web/server/lib/system-prompt/runtime.js`: opt-in managed OpenCode system-prompt optimizer materialization and plugin injection.
|
|
37
|
-
- `packages/web/server/lib/
|
|
38
|
-
- `packages/web/server/lib/opencode/managed-plugin-config.js`: the one `OPENCODE_CONFIG_CONTENT` merge every managed plugin (agent tools, system prompt optimizer, MCP reconnect) appends itself through.
|
|
37
|
+
- `packages/web/server/lib/opencode/managed-plugin-config.js`: the one `OPENCODE_CONFIG_CONTENT` merge every managed plugin (agent tools, system prompt optimizer) appends itself through.
|
|
39
38
|
- `packages/web/server/lib/opencode/server-utils-runtime.js`: shared server runtime utilities for OpenCode proxy wiring, OpenCode port/readiness helpers, and snapshot fetchers.
|
|
40
39
|
- `packages/web/server/lib/opencode/openchamber-routes.js`: AiWorker update and models metadata route registration.
|
|
41
40
|
- `packages/web/server/lib/opencode/pwa-manifest-routes.js`: PWA manifest route registration with recent-session shortcut resolution and short-lived caching.
|
|
@@ -132,8 +131,12 @@ The runtime maintains active-session count incrementally from idempotent activit
|
|
|
132
131
|
- `killProcessOnPort(port)`
|
|
133
132
|
|
|
134
133
|
Managed OpenCode launch also merges the environment returned by the agent-tool
|
|
135
|
-
runtime
|
|
136
|
-
|
|
134
|
+
runtime and the opt-in system prompt optimizer, each appending its `file://`
|
|
135
|
+
entry to the previous one's config. AiWorker adds no automatic MCP reconnect
|
|
136
|
+
loop; recovery after a failed connection is manual for both local and remote
|
|
137
|
+
servers. Previously generated reconnect plugin files are inert because managed
|
|
138
|
+
launch no longer registers them. User-configured plugins remain user-owned.
|
|
139
|
+
PATH and `OPENCODE_SERVER_PASSWORD` remain lifecycle-owned and cannot
|
|
137
140
|
be replaced by injected values. External OpenCode processes receive no
|
|
138
141
|
AiWorker tool injection. Managed launch env strips AppImage `ARGV0` before
|
|
139
142
|
spawn so zsh-backed OpenCode tools do not rewrite child argv[0] to the AppImage
|
|
@@ -156,6 +159,7 @@ Managed health failures are classified as `timeout`, `connection_refused`, `conn
|
|
|
156
159
|
## Public exports (env-runtime.js)
|
|
157
160
|
- `createOpenCodeEnvRuntime(dependencies)`: creates runtime that owns OpenCode CLI environment and binary discovery state.
|
|
158
161
|
- OpenCode CLI resolution order is persisted settings, environment overrides, bundled Desktop CLI when available, PATH, known install locations, then platform shell discovery.
|
|
162
|
+
- Automatic bundled resolution under `AIWORKER_RUNTIME=desktop` stays in runtime state and is returned to the managed launch function, including on OpenCode restart. It does not populate `process.env.OPENCODE_BINARY`: AppImage updater relaunch inherits that environment and would mistake the previous bundle path for an explicit override. Explicit settings/env selections and non-desktop or non-bundled resolution retain their existing environment behavior. This prevents future inheritance; it does not reinterpret overrides already inherited from older releases.
|
|
159
163
|
- Returned API:
|
|
160
164
|
- `applyLoginShellEnvSnapshot()`
|
|
161
165
|
- `getLoginShellEnvSnapshot()`
|
|
@@ -422,6 +426,16 @@ within a ten-minute overall deadline.
|
|
|
422
426
|
- Generic `/api/*` forwarding with hop-by-hop header filtering
|
|
423
427
|
- Windows `/session` merge fallback path behavior
|
|
424
428
|
- OpenCode readiness gate for proxied `/api` requests
|
|
429
|
+
- Worktree checkout gate before directory-scoped upstream reads and writes
|
|
430
|
+
|
|
431
|
+
Git bootstrap must reach `git-ready` before OpenCode can cache a new worktree's
|
|
432
|
+
project identity or config. Setup scripts may still be running; the optional UI
|
|
433
|
+
setup wait remains separate. Failed or timed-out checkout returns 503 without
|
|
434
|
+
forwarding. The shared draft creator keeps the project directory selected until
|
|
435
|
+
creation returns, because preview paths have no bootstrap state.
|
|
436
|
+
|
|
437
|
+
This server gate covers web, Electron, hosted mobile, and Capacitor connections.
|
|
438
|
+
The VS Code extension owns its separate Git and proxy implementation.
|
|
425
439
|
|
|
426
440
|
## Public exports (watcher.js)
|
|
427
441
|
- `createOpenCodeWatcherRuntime(dependencies)`: creates global event watcher runtime backed by the shared upstream SSE reader.
|
|
@@ -1110,7 +1110,11 @@ export const createOpenCodeEnvRuntime = (deps) => {
|
|
|
1110
1110
|
return resolved;
|
|
1111
1111
|
}
|
|
1112
1112
|
|
|
1113
|
-
process.env.
|
|
1113
|
+
// AppImage updates inherit process.env. Keep automatic desktop bundle
|
|
1114
|
+
// selection local so the next app does not treat the old mount as an override.
|
|
1115
|
+
if (process.env.AIWORKER_RUNTIME !== 'desktop' || state.resolvedOpencodeBinarySource !== 'bundled') {
|
|
1116
|
+
process.env.OPENCODE_BINARY = resolved;
|
|
1117
|
+
}
|
|
1114
1118
|
prependToPath(path.dirname(resolved));
|
|
1115
1119
|
ensureOpencodeShimRuntime(resolved);
|
|
1116
1120
|
state.resolvedOpencodeBinary = resolved;
|