@yadsh/dsh-session-scope 0.5.0 → 0.6.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.
- package/LICENSE +20 -20
- package/README.md +3 -2
- package/compatibility.json +1 -0
- package/cordis.patch.yml +7 -7
- package/lib/client.js +176 -115
- package/lib/client.js.map +1 -1
- package/lib/host-api.d.ts.map +1 -1
- package/lib/host-api.js +7 -0
- package/lib/host-api.js.map +1 -1
- package/lib/index.d.ts +2 -1
- package/lib/index.d.ts.map +1 -1
- package/lib/index.js +63 -34
- package/lib/index.js.map +1 -1
- package/lib/scope-context.d.ts.map +1 -1
- package/lib/scope-context.js +1 -5
- package/lib/scope-context.js.map +1 -1
- package/lib/scope-fs.d.ts +2 -0
- package/lib/scope-fs.d.ts.map +1 -1
- package/lib/scope-fs.js +7 -12
- package/lib/scope-fs.js.map +1 -1
- package/lib/scope-remote.d.ts +11 -0
- package/lib/scope-remote.d.ts.map +1 -0
- package/lib/scope-remote.js +35 -0
- package/lib/scope-remote.js.map +1 -0
- package/package.json +38 -31
package/LICENSE
CHANGED
|
@@ -1,22 +1,22 @@
|
|
|
1
|
-
MIT License
|
|
2
|
-
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
3
|
Copyright (c) 2026 jiangr100
|
|
4
4
|
Copyright (c) 2026 xarleyn
|
|
5
|
-
|
|
6
|
-
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
7
|
-
of this software and associated documentation files (the "Software"), to deal
|
|
8
|
-
in the Software without restriction, including without limitation the rights
|
|
9
|
-
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
10
|
-
copies of the Software, and to permit persons to whom the Software is
|
|
11
|
-
furnished to do so, subject to the following conditions:
|
|
12
|
-
|
|
13
|
-
The above copyright notice and this permission notice shall be included in all
|
|
14
|
-
copies or substantial portions of the Software.
|
|
15
|
-
|
|
16
|
-
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
17
|
-
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
18
|
-
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
19
|
-
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
20
|
-
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
21
|
-
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
22
|
-
SOFTWARE.
|
|
5
|
+
|
|
6
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
7
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
8
|
+
in the Software without restriction, including without limitation the rights
|
|
9
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
10
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
11
|
+
furnished to do so, subject to the following conditions:
|
|
12
|
+
|
|
13
|
+
The above copyright notice and this permission notice shall be included in all
|
|
14
|
+
copies or substantial portions of the Software.
|
|
15
|
+
|
|
16
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
17
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
18
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
19
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
20
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
21
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
22
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -48,11 +48,11 @@ Version `0.5.0` implements the specification through Phase 4:
|
|
|
48
48
|
|
|
49
49
|
- durable `session-scope/set` snapshots with a last-write-wins fold;
|
|
50
50
|
- canonical root validation, nested-root collapse, navigation ancestors, and stable error codes;
|
|
51
|
-
-
|
|
51
|
+
- projection-backed scope state and capabilities, a dedicated `sessionScope/list` RPC, and a write-only `/scope` command;
|
|
52
52
|
- filtered directory listings and scoped glob, grep, and search roots;
|
|
53
53
|
- per-session filesystem enforcement carried with `AsyncLocalStorage`;
|
|
54
54
|
- a monotonic final guard for known path-aware tools;
|
|
55
|
-
- model-facing context that names accessible roots without revealing hidden siblings;
|
|
55
|
+
- model-facing context that names accessible roots without revealing hidden siblings; `full` emits no scope prompt and bypasses the filesystem carrier;
|
|
56
56
|
- an independent **Scope** chip and tree picker beside Workspace and permission controls;
|
|
57
57
|
- Linux isolation for one-shot bash and persistent PTY creation;
|
|
58
58
|
- permission-aware read-only and writable mounts in an empty workspace overlay;
|
|
@@ -70,6 +70,7 @@ Version `0.5.0` implements the specification through Phase 4:
|
|
|
70
70
|
- Isolated scope requires the supported Linux sandbox path and a recognized bubblewrap profile.
|
|
71
71
|
- Unknown runner profiles, partial enforcement, and unsupported platforms fail closed.
|
|
72
72
|
- Scope changes are blocked while foreground jobs, background jobs, or persistent terminals retain the previous mount view.
|
|
73
|
+
- Opening or refreshing the Scope editor is read-only: it uses projections and `sessionScope/list`, so it does not add command rows to session history. Applying an unchanged scope is a no-op.
|
|
73
74
|
|
|
74
75
|
## Requirements
|
|
75
76
|
|
package/compatibility.json
CHANGED
package/cordis.patch.yml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
# dsh-session-scope bundle patch — mounts the plugin row into the
|
|
2
|
-
# profile composition. The `dsh.bundle.patch` manifest field points here, so
|
|
3
|
-
# `dsh plugin --profile web add <this directory>` activates this row
|
|
4
|
-
# automatically on the next boot.
|
|
5
|
-
- insert:
|
|
6
|
-
- id: dsh-session-scope
|
|
7
|
-
name: "@yadsh/dsh-session-scope"
|
|
1
|
+
# dsh-session-scope bundle patch — mounts the plugin row into the
|
|
2
|
+
# profile composition. The `dsh.bundle.patch` manifest field points here, so
|
|
3
|
+
# `dsh plugin --profile web add <this directory>` activates this row
|
|
4
|
+
# automatically on the next boot.
|
|
5
|
+
- insert:
|
|
6
|
+
- id: dsh-session-scope
|
|
7
|
+
name: "@yadsh/dsh-session-scope"
|
package/lib/client.js
CHANGED
|
@@ -6,16 +6,16 @@
|
|
|
6
6
|
// with the web shell's module loader (window.__ModuleLoader__) and exports a
|
|
7
7
|
// Cordis client plugin. It requires only `react` and `react-dom` (both are
|
|
8
8
|
// shell statics); everything else comes from client services (`slots`,
|
|
9
|
-
// `
|
|
9
|
+
// `remote`, `sessions`).
|
|
10
10
|
//
|
|
11
11
|
// It contributes an independent Scope chip beside the Workspace picker while
|
|
12
12
|
// a session is blank and beside the permission selector after the first turn.
|
|
13
|
-
// The editor consumes the `session-scope`
|
|
14
|
-
// complete snapshots through `/scope`; permission and
|
|
15
|
-
// state.
|
|
16
|
-
// - The editor walks the directory tree
|
|
17
|
-
//
|
|
18
|
-
//
|
|
13
|
+
// The editor consumes state and host capabilities from the `session-scope`
|
|
14
|
+
// projection and writes complete snapshots through `/scope`; permission and
|
|
15
|
+
// scope never share UI state.
|
|
16
|
+
// - The editor walks the directory tree beneath the session workspace and
|
|
17
|
+
// toggles which directories the agent may write to, in addition to the
|
|
18
|
+
// session workspace. Changes go through the host's
|
|
19
19
|
// `/scope` command; the `session-scope` session projection
|
|
20
20
|
// pushes the state back, so the button and the tree stay in sync with the
|
|
21
21
|
// server.
|
|
@@ -24,12 +24,10 @@
|
|
|
24
24
|
// z-index, because the composer seat is `position: sticky` inside its own
|
|
25
25
|
// stacking context — an in-place fixed overlay would be clipped or buried.
|
|
26
26
|
//
|
|
27
|
-
// Directory listings come from the
|
|
28
|
-
//
|
|
29
|
-
// serves the native picker instead, the tree falls back to the plugin's own
|
|
30
|
-
// `/scope list` host command.
|
|
27
|
+
// Directory listings come from the plugin's dedicated `sessionScope/list`
|
|
28
|
+
// RPC. Read-only UI refreshes never execute durable slash commands.
|
|
31
29
|
window.__ModuleLoader__.load({
|
|
32
|
-
id: 'dsh-session-scope',
|
|
30
|
+
id: '@yadsh/dsh-session-scope',
|
|
33
31
|
factory: (require) => {
|
|
34
32
|
var module = { exports: {} };
|
|
35
33
|
var exports = module.exports;
|
|
@@ -60,6 +58,7 @@ window.__ModuleLoader__.load({
|
|
|
60
58
|
'.wss-mode { border: 1px solid var(--dsw-alias-border-l2); border-radius: 8px; background: transparent; color: var(--dsw-alias-label-secondary); padding: 7px 8px; font: inherit; font-size: 12px; cursor: pointer; }',
|
|
61
59
|
'.wss-mode:hover:not(:disabled) { background: var(--dsw-alias-interactive-bg-hover); }',
|
|
62
60
|
'.wss-modeOn { border-color: var(--dsw-alias-state-business-primary); color: var(--dsw-alias-label-primary); background: var(--dsw-alias-fill-tsp-secondary); }',
|
|
61
|
+
'.wss-modeUnavailable { opacity: .55; }',
|
|
63
62
|
'.wss-crumbs { display: flex; align-items: center; gap: 2px; flex-wrap: wrap; font-size: 12px; line-height: 18px; }',
|
|
64
63
|
'.wss-crumb { border: none; background: transparent; color: var(--dsw-alias-label-secondary); cursor: pointer; padding: 1px 4px; border-radius: 6px; font: inherit; }',
|
|
65
64
|
'.wss-crumb:hover { background: var(--dsw-alias-interactive-bg-hover); color: var(--dsw-alias-label-primary); }',
|
|
@@ -106,13 +105,18 @@ window.__ModuleLoader__.load({
|
|
|
106
105
|
function sepOf(path) {
|
|
107
106
|
return path.indexOf('\\') !== -1 ? '\\' : '/';
|
|
108
107
|
}
|
|
108
|
+
function comparablePath(path) {
|
|
109
|
+
return sepOf(path) === '\\' ? path.toLowerCase() : path;
|
|
110
|
+
}
|
|
109
111
|
// Whether `path` is `root` or lies beneath it (separator-aware prefix).
|
|
110
112
|
function isUnder(path, root) {
|
|
111
|
-
|
|
113
|
+
var comparableTarget = comparablePath(path);
|
|
114
|
+
var comparableRoot = comparablePath(root);
|
|
115
|
+
if (comparableTarget === comparableRoot)
|
|
112
116
|
return true;
|
|
113
117
|
var sep = sepOf(root);
|
|
114
|
-
var prefix =
|
|
115
|
-
return
|
|
118
|
+
var prefix = comparableRoot.endsWith(sep) ? comparableRoot : comparableRoot + sep;
|
|
119
|
+
return comparableTarget.indexOf(prefix) === 0;
|
|
116
120
|
}
|
|
117
121
|
// The deepest selected root that covers `path`, or undefined.
|
|
118
122
|
function coveringRoot(path, roots) {
|
|
@@ -129,9 +133,89 @@ window.__ModuleLoader__.load({
|
|
|
129
133
|
var parts = path.split(sep).filter(Boolean);
|
|
130
134
|
return parts.length === 0 ? path : parts[parts.length - 1];
|
|
131
135
|
}
|
|
136
|
+
// Render paths relative to the session workspace. Absolute paths remain
|
|
137
|
+
// host-side implementation details and never need to appear in the picker.
|
|
138
|
+
function displayPath(path, root) {
|
|
139
|
+
if (typeof path !== 'string' || typeof root !== 'string' || !isUnder(path, root))
|
|
140
|
+
return baseName(path);
|
|
141
|
+
if (comparablePath(path) === comparablePath(root))
|
|
142
|
+
return '.';
|
|
143
|
+
var relative = path.slice(root.length);
|
|
144
|
+
var sep = sepOf(root);
|
|
145
|
+
while (relative.startsWith(sep))
|
|
146
|
+
relative = relative.slice(1);
|
|
147
|
+
return relative.split(sep).join('/');
|
|
148
|
+
}
|
|
132
149
|
function normalizeDraftRoots(roots) {
|
|
133
150
|
return Array.isArray(roots) ? roots.filter(function (root) { return typeof root === 'string'; }) : [];
|
|
134
151
|
}
|
|
152
|
+
function comparableScopeRoots(roots) {
|
|
153
|
+
var ordered = normalizeDraftRoots(roots).slice().sort(function (left, right) {
|
|
154
|
+
return left.length - right.length || comparablePath(left).localeCompare(comparablePath(right));
|
|
155
|
+
});
|
|
156
|
+
var collapsed = [];
|
|
157
|
+
for (var i = 0; i < ordered.length; i++) {
|
|
158
|
+
if (!collapsed.some(function (root) { return isUnder(ordered[i], root); }))
|
|
159
|
+
collapsed.push(ordered[i]);
|
|
160
|
+
}
|
|
161
|
+
return collapsed.sort(function (left, right) { return comparablePath(left).localeCompare(comparablePath(right)); });
|
|
162
|
+
}
|
|
163
|
+
function sameRoots(left, right) {
|
|
164
|
+
var a = comparableScopeRoots(left);
|
|
165
|
+
var b = comparableScopeRoots(right);
|
|
166
|
+
return a.length === b.length && a.every(function (root, index) {
|
|
167
|
+
return comparablePath(root) === comparablePath(b[index]);
|
|
168
|
+
});
|
|
169
|
+
}
|
|
170
|
+
var stringSchema = {
|
|
171
|
+
parse: function (value) {
|
|
172
|
+
if (typeof value !== 'string' || value.length === 0)
|
|
173
|
+
throw new TypeError('expected a non-empty string');
|
|
174
|
+
return value;
|
|
175
|
+
},
|
|
176
|
+
};
|
|
177
|
+
var directoryListingSchema = {
|
|
178
|
+
parse: function (value) {
|
|
179
|
+
if (value === null || typeof value !== 'object')
|
|
180
|
+
throw new TypeError('expected a directory listing');
|
|
181
|
+
if (typeof value.path !== 'string' || typeof value.home !== 'string')
|
|
182
|
+
throw new TypeError('invalid directory listing roots');
|
|
183
|
+
if (!Array.isArray(value.crumbs) || !Array.isArray(value.entries))
|
|
184
|
+
throw new TypeError('invalid directory listing rows');
|
|
185
|
+
return {
|
|
186
|
+
path: value.path,
|
|
187
|
+
home: value.home,
|
|
188
|
+
crumbs: value.crumbs.map(function (crumb) {
|
|
189
|
+
if (crumb === null || typeof crumb !== 'object' || typeof crumb.name !== 'string' || typeof crumb.path !== 'string') {
|
|
190
|
+
throw new TypeError('invalid directory crumb');
|
|
191
|
+
}
|
|
192
|
+
return { name: crumb.name, path: crumb.path };
|
|
193
|
+
}),
|
|
194
|
+
entries: value.entries.map(function (entry) {
|
|
195
|
+
if (entry === null || typeof entry !== 'object' || typeof entry.name !== 'string' || typeof entry.path !== 'string') {
|
|
196
|
+
throw new TypeError('invalid directory entry');
|
|
197
|
+
}
|
|
198
|
+
return { name: entry.name, path: entry.path, hidden: entry.hidden === true };
|
|
199
|
+
}),
|
|
200
|
+
truncated: value.truncated === true,
|
|
201
|
+
};
|
|
202
|
+
},
|
|
203
|
+
};
|
|
204
|
+
var scopeRemoteContribution = {
|
|
205
|
+
package: '@yadsh/dsh-session-scope',
|
|
206
|
+
descriptors: [{
|
|
207
|
+
id: '@yadsh/dsh-session-scope#sessionScope/list',
|
|
208
|
+
service: 'sessionScopeRead',
|
|
209
|
+
namespace: 'sessionScope',
|
|
210
|
+
method: 'list',
|
|
211
|
+
invocation: { kind: 'direct' },
|
|
212
|
+
parameters: [
|
|
213
|
+
{ name: 'sessionId', wire: 'sessionId', source: 'json', codec: { mode: 'strict', typeSymbol: 'SessionId', schema: stringSchema } },
|
|
214
|
+
{ name: 'path', wire: 'path', source: 'json', codec: { mode: 'strict', typeSymbol: 'string', schema: stringSchema } },
|
|
215
|
+
],
|
|
216
|
+
result: { mode: 'strict', typeSymbol: 'DirectoryListing', schema: directoryListingSchema },
|
|
217
|
+
}],
|
|
218
|
+
};
|
|
135
219
|
function apply(ctx) {
|
|
136
220
|
var styleTag = null;
|
|
137
221
|
try {
|
|
@@ -140,18 +224,29 @@ window.__ModuleLoader__.load({
|
|
|
140
224
|
document.head.appendChild(styleTag);
|
|
141
225
|
}
|
|
142
226
|
catch (err) { /* styling is cosmetic */ }
|
|
143
|
-
function connection() {
|
|
144
|
-
var value = ctx.get('connection');
|
|
145
|
-
return value !== undefined && value !== null ? value : undefined;
|
|
146
|
-
}
|
|
147
227
|
function remote() {
|
|
148
228
|
var value = ctx.get('remote');
|
|
149
229
|
return value !== undefined && value !== null ? value : undefined;
|
|
150
230
|
}
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
231
|
+
var scopeRemoteDispose = null;
|
|
232
|
+
var scopeRemoteError = null;
|
|
233
|
+
var scopeRemoteFace = null;
|
|
234
|
+
var rem = remote();
|
|
235
|
+
var scopeRemoteReady = rem !== undefined && typeof rem.$mount === 'function'
|
|
236
|
+
? rem.$mount(scopeRemoteContribution).then(function (dispose) {
|
|
237
|
+
scopeRemoteDispose = dispose;
|
|
238
|
+
if (typeof ctx.inject !== 'function')
|
|
239
|
+
throw new Error('session-scope: client injection unavailable');
|
|
240
|
+
return ctx.inject(['remote.sessionScope'], function (remoteCtx) {
|
|
241
|
+
var injectedRemote = remoteCtx.get('remote');
|
|
242
|
+
scopeRemoteFace = injectedRemote.sessionScope;
|
|
243
|
+
});
|
|
244
|
+
}).catch(function (err) {
|
|
245
|
+
scopeRemoteError = err instanceof Error ? err.message : String(err);
|
|
246
|
+
})
|
|
247
|
+
: Promise.resolve().then(function () {
|
|
248
|
+
scopeRemoteError = 'session-scope: Remote gateway unavailable';
|
|
249
|
+
});
|
|
155
250
|
// Execute one slash-command and return { ok, result } where result is
|
|
156
251
|
// the normalized { kind, text } command result when the host answered.
|
|
157
252
|
async function runCommand(sessionId, line) {
|
|
@@ -179,49 +274,35 @@ window.__ModuleLoader__.load({
|
|
|
179
274
|
return { ok: false, error: err instanceof Error ? err.message : String(err) };
|
|
180
275
|
}
|
|
181
276
|
}
|
|
182
|
-
// List one directory level
|
|
183
|
-
// capability). Fallback: the plugin's own /scope list
|
|
184
|
-
// command, used when the composition serves the native picker.
|
|
277
|
+
// List one directory level through the dedicated host RPC.
|
|
185
278
|
async function listLevel(sessionId, path) {
|
|
186
|
-
|
|
187
|
-
if (
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
return { ok: true, value: response.result.value, source: 'browse' };
|
|
192
|
-
}
|
|
193
|
-
if (response !== undefined && response.result !== undefined && response.result.error !== undefined) {
|
|
194
|
-
var code = response.result.error.code;
|
|
195
|
-
if (code === 'directory-picker-unavailable') {
|
|
196
|
-
return fallbackList(sessionId, path);
|
|
197
|
-
}
|
|
198
|
-
return { ok: false, error: response.result.error.message };
|
|
199
|
-
}
|
|
200
|
-
}
|
|
201
|
-
catch (err) {
|
|
202
|
-
return { ok: false, error: err instanceof Error ? err.message : String(err) };
|
|
203
|
-
}
|
|
279
|
+
await scopeRemoteReady;
|
|
280
|
+
if (scopeRemoteError !== null)
|
|
281
|
+
return { ok: false, error: scopeRemoteError };
|
|
282
|
+
if (scopeRemoteFace === null || typeof scopeRemoteFace.list !== 'function') {
|
|
283
|
+
return { ok: false, error: 'session-scope: read RPC unavailable' };
|
|
204
284
|
}
|
|
205
|
-
return fallbackList(sessionId, path);
|
|
206
|
-
}
|
|
207
|
-
async function fallbackList(sessionId, path) {
|
|
208
|
-
var outcome = await runCommand(sessionId, '/scope list ' + path);
|
|
209
|
-
if (!outcome.ok)
|
|
210
|
-
return { ok: false, error: outcome.error };
|
|
211
285
|
try {
|
|
212
|
-
|
|
286
|
+
var response = await scopeRemoteFace.list(sessionId, path);
|
|
287
|
+
if (response !== undefined && response.ok === true) {
|
|
288
|
+
return { ok: true, value: response.value, source: 'scope-rpc' };
|
|
289
|
+
}
|
|
290
|
+
var message = response !== undefined && response.error !== undefined && response.error.message !== undefined
|
|
291
|
+
? response.error.message
|
|
292
|
+
: 'session-scope: directory listing failed';
|
|
293
|
+
return { ok: false, error: message };
|
|
213
294
|
}
|
|
214
295
|
catch (err) {
|
|
215
|
-
return { ok: false, error:
|
|
296
|
+
return { ok: false, error: err instanceof Error ? err.message : String(err) };
|
|
216
297
|
}
|
|
217
298
|
}
|
|
218
299
|
// ---------- the scope editor (modal with the directory tree) ----------
|
|
219
300
|
function ScopeEditor(props) {
|
|
220
301
|
// props: sessionId, workspaceRoot (injected cwd, may be undefined),
|
|
221
|
-
// projectedRoot, scopeMode, scopeRoots, onClose
|
|
302
|
+
// projectedRoot, scopeMode, scopeRoots, capabilities, onClose
|
|
222
303
|
var state = React.useState({
|
|
223
304
|
root: null,
|
|
224
|
-
rootSource: null, // 'injected' | 'projection'
|
|
305
|
+
rootSource: null, // 'injected' | 'projection'
|
|
225
306
|
path: null,
|
|
226
307
|
listing: null, // { path, crumbs, entries, truncated }
|
|
227
308
|
loading: false,
|
|
@@ -229,7 +310,7 @@ window.__ModuleLoader__.load({
|
|
|
229
310
|
error: null,
|
|
230
311
|
phase: L('正在解析工作区…', 'Resolving workspace…'),
|
|
231
312
|
retryToken: 0,
|
|
232
|
-
isolatedSupported: null,
|
|
313
|
+
isolatedSupported: props.capabilities !== undefined ? props.capabilities.isolated === true : null,
|
|
233
314
|
mode: props.scopeMode === 'focused' || props.scopeMode === 'isolated' ? props.scopeMode : 'full',
|
|
234
315
|
// Local pending content roots. In full mode the workspace root is
|
|
235
316
|
// inserted after root resolution to keep the checkbox semantics.
|
|
@@ -238,27 +319,15 @@ window.__ModuleLoader__.load({
|
|
|
238
319
|
var snap = state[0];
|
|
239
320
|
var setSnap = state[1];
|
|
240
321
|
var patch = function (part) { setSnap(function (prev) { return Object.assign({}, prev, part); }); };
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
runCommand(props.sessionId, '/scope capabilities').then(function (outcome) {
|
|
244
|
-
if (cancelled || !outcome.ok)
|
|
245
|
-
return;
|
|
246
|
-
try {
|
|
247
|
-
var capabilities = JSON.parse(outcome.result.text);
|
|
248
|
-
patch({ isolatedSupported: capabilities.isolated === true });
|
|
249
|
-
}
|
|
250
|
-
catch (err) { /* keep unknown capability state */ }
|
|
251
|
-
});
|
|
252
|
-
return function () { cancelled = true; };
|
|
253
|
-
}, [props.sessionId]);
|
|
254
|
-
// Resolve the workspace root through a chain of sources: the injected
|
|
255
|
-
// session cwd, the session-scope projection root, then `/scope show`.
|
|
322
|
+
// Resolve the immutable workspace root from the session list or the
|
|
323
|
+
// session-scope projection. No read command is issued from the UI.
|
|
256
324
|
function applyRoot(root, source, mode, roots) {
|
|
257
325
|
var nextMode = mode === 'focused' || mode === 'isolated' ? mode : 'full';
|
|
258
326
|
patch({
|
|
259
327
|
root: root,
|
|
260
328
|
rootSource: source,
|
|
261
329
|
phase: L('正在加载目录…', 'Loading directories…'),
|
|
330
|
+
error: null,
|
|
262
331
|
mode: nextMode,
|
|
263
332
|
draft: nextMode === 'full' ? [root] : normalizeDraftRoots(roots),
|
|
264
333
|
});
|
|
@@ -280,34 +349,7 @@ window.__ModuleLoader__.load({
|
|
|
280
349
|
applyRoot(projected, 'projection', snap.mode, snap.draft);
|
|
281
350
|
return;
|
|
282
351
|
}
|
|
283
|
-
patch({
|
|
284
|
-
timer = setTimeout(function () {
|
|
285
|
-
if (cancelled || snap.root !== null)
|
|
286
|
-
return;
|
|
287
|
-
patch({ loading: false, error: L('解析工作区超时 — 请重试', 'resolving the workspace timed out — please retry'), phase: null });
|
|
288
|
-
}, 12000);
|
|
289
|
-
var outcome = await runCommand(props.sessionId, '/scope show');
|
|
290
|
-
if (cancelled)
|
|
291
|
-
return;
|
|
292
|
-
if (timer !== null) {
|
|
293
|
-
clearTimeout(timer);
|
|
294
|
-
timer = null;
|
|
295
|
-
}
|
|
296
|
-
if (!outcome.ok) {
|
|
297
|
-
patch({ loading: false, error: outcome.error, phase: null });
|
|
298
|
-
return;
|
|
299
|
-
}
|
|
300
|
-
var info = null;
|
|
301
|
-
try {
|
|
302
|
-
info = JSON.parse(outcome.result.text);
|
|
303
|
-
}
|
|
304
|
-
catch (err) { /* invalid */ }
|
|
305
|
-
var root = info !== null && typeof info.workspaceRoot === 'string' && info.workspaceRoot !== '' ? info.workspaceRoot : null;
|
|
306
|
-
if (root === null) {
|
|
307
|
-
patch({ loading: false, error: L('无法解析工作区根目录', 'could not resolve the workspace root'), phase: null });
|
|
308
|
-
return;
|
|
309
|
-
}
|
|
310
|
-
applyRoot(root, 'info', info.mode, info.roots);
|
|
352
|
+
patch({ loading: false, error: L('无法解析工作区根目录', 'could not resolve the workspace root'), phase: null });
|
|
311
353
|
}
|
|
312
354
|
catch (err) {
|
|
313
355
|
if (cancelled)
|
|
@@ -399,6 +441,13 @@ window.__ModuleLoader__.load({
|
|
|
399
441
|
// and a rejection handler both settle the flag and surface a visible
|
|
400
442
|
// error, keeping the modal open with the draft intact.
|
|
401
443
|
function save(mode, draft) {
|
|
444
|
+
var effectiveMode = snap.root !== null && draft.indexOf(snap.root) !== -1 ? 'full' : mode;
|
|
445
|
+
var effectiveRoots = effectiveMode === 'full' ? [] : normalizeDraftRoots(draft);
|
|
446
|
+
var currentRoots = normalizeDraftRoots(props.scopeRoots);
|
|
447
|
+
if (effectiveMode === props.scopeMode && sameRoots(effectiveRoots, currentRoots)) {
|
|
448
|
+
props.onClose();
|
|
449
|
+
return;
|
|
450
|
+
}
|
|
402
451
|
patch({ saving: true, error: null });
|
|
403
452
|
var settled = false;
|
|
404
453
|
var timer = setTimeout(function () {
|
|
@@ -407,7 +456,6 @@ window.__ModuleLoader__.load({
|
|
|
407
456
|
settled = true;
|
|
408
457
|
patch({ saving: false, error: L('保存超时 — 请重试', 'saving timed out — please retry') });
|
|
409
458
|
}, 12000);
|
|
410
|
-
var effectiveMode = snap.root !== null && draft.indexOf(snap.root) !== -1 ? 'full' : mode;
|
|
411
459
|
var command = effectiveMode === 'full'
|
|
412
460
|
? '/scope full'
|
|
413
461
|
: '/scope ' + effectiveMode + ' ' + JSON.stringify(draft);
|
|
@@ -458,8 +506,12 @@ window.__ModuleLoader__.load({
|
|
|
458
506
|
patch({ draft: next, error: null });
|
|
459
507
|
}
|
|
460
508
|
function selectMode(mode) {
|
|
461
|
-
if (mode === 'isolated' && snap.isolatedSupported === false)
|
|
509
|
+
if (mode === 'isolated' && snap.isolatedSupported === false) {
|
|
510
|
+
patch({
|
|
511
|
+
error: L('隔离模式需要支持 bubblewrap 的 Linux 主机。此主机仍可使用聚焦模式。', 'Isolated mode requires a Linux host with supported bubblewrap. Focused mode remains available on this host.'),
|
|
512
|
+
});
|
|
462
513
|
return;
|
|
514
|
+
}
|
|
463
515
|
if (mode === 'full') {
|
|
464
516
|
patch({ mode: 'full', draft: snap.root === null ? [] : [snap.root], error: null });
|
|
465
517
|
return;
|
|
@@ -495,6 +547,7 @@ window.__ModuleLoader__.load({
|
|
|
495
547
|
var listing = snap.listing;
|
|
496
548
|
var crumbs = listing !== null ? listing.crumbs : [];
|
|
497
549
|
var entries = listing !== null ? listing.entries : [];
|
|
550
|
+
var visibleCrumbs = snap.root === null ? [] : crumbs.filter(function (crumb) { return isUnder(crumb.path, snap.root); });
|
|
498
551
|
var overlay = React.createElement('div', { className: 'wss-overlay' }, React.createElement('div', { className: 'wss-modal', ref: modalRef, role: 'dialog', 'aria-label': L('会话范围', 'Session scope') }, React.createElement('div', { className: 'wss-head' }, React.createElement('span', { className: 'wss-title' }, L('会话范围', 'Session Scope')), React.createElement('button', { type: 'button', className: 'wss-close', onClick: props.onClose, 'aria-label': L('关闭', 'Close') }, IconClose())), React.createElement('div', { className: 'wss-caption' }, L('范围控制 agent 可以看到的工作区部分,与读写权限无关。Focused 限制 DSH 文件工具;Isolated 还限制受支持的 shell 进程。', 'Scope controls which workspace areas the agent can see, independently from read/write permission. Focused restricts DSH filesystem tools; Isolated also confines supported shell processes.')), React.createElement('div', { className: 'wss-modes', role: 'radiogroup', 'aria-label': L('范围模式', 'Scope mode') }, [
|
|
499
552
|
{ value: 'full', label: L('整个工作区', 'Entire workspace') },
|
|
500
553
|
{ value: 'focused', label: L('聚焦', 'Focused') },
|
|
@@ -506,9 +559,10 @@ window.__ModuleLoader__.load({
|
|
|
506
559
|
type: 'button',
|
|
507
560
|
role: 'radio',
|
|
508
561
|
'aria-checked': snap.mode === option.value,
|
|
509
|
-
className: 'wss-mode' + (snap.mode === option.value ? ' wss-modeOn' : ''),
|
|
510
|
-
disabled: snap.saving
|
|
511
|
-
|
|
562
|
+
className: 'wss-mode' + (snap.mode === option.value ? ' wss-modeOn' : '') + (unavailable ? ' wss-modeUnavailable' : ''),
|
|
563
|
+
disabled: snap.saving,
|
|
564
|
+
'aria-disabled': unavailable,
|
|
565
|
+
title: unavailable ? L('需要支持 bubblewrap 的 Linux 主机', 'Requires a Linux host with supported bubblewrap') : option.label,
|
|
512
566
|
onClick: function () { selectMode(option.value); },
|
|
513
567
|
}, option.label);
|
|
514
568
|
})), snap.phase !== null && React.createElement('div', { className: 'wss-busy' }, snap.phase), snap.error !== null && React.createElement('div', { className: 'wss-error' }, snap.error, React.createElement('button', {
|
|
@@ -516,30 +570,30 @@ window.__ModuleLoader__.load({
|
|
|
516
570
|
className: 'wss-btn wss-btnGhost',
|
|
517
571
|
onClick: retry,
|
|
518
572
|
style: { marginLeft: 8 },
|
|
519
|
-
}, L('重试', 'Retry'))), snap.root !== null && React.createElement('div', { className: 'wss-crumbs' },
|
|
573
|
+
}, L('重试', 'Retry'))), snap.root !== null && React.createElement('div', { className: 'wss-crumbs' }, visibleCrumbs.map(function (crumb, index) {
|
|
520
574
|
return React.createElement(React.Fragment, { key: crumb.path }, index > 0 && React.createElement('span', { className: 'wss-crumbSep' }, '/'), React.createElement('button', {
|
|
521
575
|
type: 'button',
|
|
522
576
|
className: 'wss-crumb',
|
|
523
|
-
title: crumb.path,
|
|
577
|
+
title: displayPath(crumb.path, snap.root),
|
|
524
578
|
onClick: function () {
|
|
525
579
|
if (crumb.path !== snap.path)
|
|
526
580
|
enter(crumb.path);
|
|
527
581
|
},
|
|
528
|
-
}, crumb.name));
|
|
582
|
+
}, index === 0 ? '.' : crumb.name));
|
|
529
583
|
})), React.createElement('div', { className: 'wss-tree' }, snap.loading && React.createElement('div', { className: 'wss-busy' }, L('加载中…', 'Loading…')), !snap.loading && snap.path !== null && React.createElement('div', { className: 'wss-row' }, React.createElement('button', {
|
|
530
584
|
type: 'button',
|
|
531
585
|
className: 'wss-check' + (coveringRoot(snap.path, snap.draft) !== undefined ? ' wss-checkOn' : ''),
|
|
532
586
|
disabled: snap.saving || (coveringRoot(snap.path, snap.draft) !== undefined && snap.draft.indexOf(snap.path) === -1),
|
|
533
|
-
'aria-label': L('切换目录', 'Toggle directory') + ' ' + snap.path,
|
|
587
|
+
'aria-label': L('切换目录', 'Toggle directory') + ' ' + displayPath(snap.path, snap.root),
|
|
534
588
|
title: coveringRoot(snap.path, snap.draft) !== undefined && snap.draft.indexOf(snap.path) === -1
|
|
535
589
|
? L('经父目录包含:取消父目录后整个子树将不可见', 'Included via a parent directory; uncheck the parent to hide its whole subtree')
|
|
536
590
|
: snap.path === snap.root
|
|
537
591
|
? (snap.draft.indexOf(snap.path) !== -1
|
|
538
592
|
? L('整个工作区可见 — 取消勾选后选择聚焦范围', 'The entire workspace is visible — uncheck to choose a focused scope')
|
|
539
593
|
: L('勾选以显示整个工作区', 'Check to expose the entire workspace'))
|
|
540
|
-
: snap.path,
|
|
594
|
+
: displayPath(snap.path, snap.root),
|
|
541
595
|
onClick: function (ev) { ev.stopPropagation(); toggle(snap.path); },
|
|
542
|
-
}, coveringRoot(snap.path, snap.draft) !== undefined ? IconCheck() : null), React.createElement('span', { className: 'wss-rowName' }, IconFolder(), React.createElement('span', { style: { minWidth: 0, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' } }, snap.path)), coveringRoot(snap.path, snap.draft) !== undefined && snap.draft.indexOf(snap.path) === -1 &&
|
|
596
|
+
}, coveringRoot(snap.path, snap.draft) !== undefined ? IconCheck() : null), React.createElement('span', { className: 'wss-rowName' }, IconFolder(), React.createElement('span', { style: { minWidth: 0, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' } }, displayPath(snap.path, snap.root))), coveringRoot(snap.path, snap.draft) !== undefined && snap.draft.indexOf(snap.path) === -1 &&
|
|
543
597
|
React.createElement('span', { className: 'wss-hint' }, L('经父目录包含', 'via parent')), snap.path === snap.root && coveringRoot(snap.path, snap.draft) === undefined &&
|
|
544
598
|
React.createElement('span', { className: 'wss-hint' }, L('聚焦范围', 'focused scope'))), !snap.loading && snap.path !== null && entries.map(function (entry) {
|
|
545
599
|
var covered = coveringRoot(entry.path, snap.draft);
|
|
@@ -553,12 +607,12 @@ window.__ModuleLoader__.load({
|
|
|
553
607
|
type: 'button',
|
|
554
608
|
className: 'wss-check' + (on ? ' wss-checkOn' : ''),
|
|
555
609
|
disabled: snap.saving || (on && !self),
|
|
556
|
-
'aria-label': L('切换目录', 'Toggle directory') + ' ' + entry.path,
|
|
557
|
-
title: on && !self ? L('经父目录包含:取消父目录后整个子树将不可见', 'Included via a parent directory; uncheck the parent to hide its whole subtree') : entry.path,
|
|
610
|
+
'aria-label': L('切换目录', 'Toggle directory') + ' ' + displayPath(entry.path, snap.root),
|
|
611
|
+
title: on && !self ? L('经父目录包含:取消父目录后整个子树将不可见', 'Included via a parent directory; uncheck the parent to hide its whole subtree') : displayPath(entry.path, snap.root),
|
|
558
612
|
onClick: function (ev) { ev.stopPropagation(); toggle(entry.path); },
|
|
559
613
|
}, on ? IconCheck() : null), React.createElement('span', { className: 'wss-rowName' + (entry.hidden ? ' wss-rowNameDim' : '') }, IconFolder(), React.createElement('span', { style: { minWidth: 0, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' } }, entry.name)), on && !self && React.createElement('span', { className: 'wss-hint' }, L('经父目录', 'via parent')), React.createElement('span', { className: 'wss-chevron' }, IconChevron()));
|
|
560
614
|
}), !snap.loading && snap.path !== null && entries.length === 0 &&
|
|
561
|
-
React.createElement('div', { className: 'wss-empty' }, L('(无子目录)', '(no subdirectories)'))), React.createElement('div', { className: 'wss-foot' }, React.createElement('span', { className: 'wss-footRoots', title: snap.draft.join('\n') }, (snap.mode === 'full'
|
|
615
|
+
React.createElement('div', { className: 'wss-empty' }, L('(无子目录)', '(no subdirectories)'))), React.createElement('div', { className: 'wss-foot' }, React.createElement('span', { className: 'wss-footRoots', title: snap.draft.map(function (root) { return displayPath(root, snap.root); }).join('\n') }, (snap.mode === 'full'
|
|
562
616
|
? L('整个工作区 · ', 'entire workspace · ')
|
|
563
617
|
: (snap.mode === 'isolated' ? L('隔离 · ', 'isolated · ') : L('聚焦 · ', 'focused · '))) +
|
|
564
618
|
(snap.draft.length === 0
|
|
@@ -630,6 +684,7 @@ window.__ModuleLoader__.load({
|
|
|
630
684
|
var roots = scope !== undefined && Array.isArray(scope.roots) ? scope.roots : [];
|
|
631
685
|
var projectedRoot = scope !== undefined && typeof scope.workspaceRoot === 'string' && scope.workspaceRoot !== '' ? scope.workspaceRoot : undefined;
|
|
632
686
|
var mode = scope !== undefined && (scope.mode === 'focused' || scope.mode === 'isolated') ? scope.mode : 'full';
|
|
687
|
+
var capabilities = scope !== undefined && scope.capabilities !== undefined ? scope.capabilities : undefined;
|
|
633
688
|
var label = mode === 'full'
|
|
634
689
|
? L('范围:全部', 'Scope: All')
|
|
635
690
|
: roots.length === 0
|
|
@@ -658,6 +713,7 @@ window.__ModuleLoader__.load({
|
|
|
658
713
|
projectedRoot: projectedRoot,
|
|
659
714
|
scopeMode: mode,
|
|
660
715
|
scopeRoots: roots,
|
|
716
|
+
capabilities: capabilities,
|
|
661
717
|
onClose: function () { setOpen(false); },
|
|
662
718
|
}));
|
|
663
719
|
}
|
|
@@ -668,8 +724,7 @@ window.__ModuleLoader__.load({
|
|
|
668
724
|
function scopeInjection(sessionId) {
|
|
669
725
|
// The session's workspace root never changes; the sessions list
|
|
670
726
|
// store (byId, keyed by session id) is the cheapest reliable
|
|
671
|
-
// source. The editor falls back to the projection
|
|
672
|
-
// /scope show.
|
|
727
|
+
// source. The editor falls back to the session-scope projection.
|
|
673
728
|
var root = undefined;
|
|
674
729
|
try {
|
|
675
730
|
var sessions = ctx.get('sessions');
|
|
@@ -704,12 +759,18 @@ window.__ModuleLoader__.load({
|
|
|
704
759
|
}
|
|
705
760
|
catch (err) { /* best effort */ }
|
|
706
761
|
}
|
|
762
|
+
if (scopeRemoteDispose !== null) {
|
|
763
|
+
try {
|
|
764
|
+
void scopeRemoteDispose();
|
|
765
|
+
}
|
|
766
|
+
catch (err) { /* best effort */ }
|
|
767
|
+
}
|
|
707
768
|
if (styleTag !== null && styleTag.parentNode !== null)
|
|
708
769
|
styleTag.parentNode.removeChild(styleTag);
|
|
709
770
|
};
|
|
710
771
|
}
|
|
711
772
|
exports.apply = apply;
|
|
712
|
-
exports.inject = ['slots', '
|
|
773
|
+
exports.inject = ['slots', 'remote', 'remote.commands', 'sessions'];
|
|
713
774
|
return module.exports;
|
|
714
775
|
},
|
|
715
776
|
});
|