@yadsh/dsh-session-scope 0.5.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 +22 -0
- package/README.md +110 -0
- package/compatibility.json +20 -0
- package/cordis.patch.yml +7 -0
- package/lib/client.d.ts +1 -0
- package/lib/client.d.ts.map +1 -0
- package/lib/client.js +716 -0
- package/lib/client.js.map +1 -0
- package/lib/core.d.ts +204 -0
- package/lib/core.d.ts.map +1 -0
- package/lib/core.js +375 -0
- package/lib/core.js.map +1 -0
- package/lib/host-api.d.ts +23 -0
- package/lib/host-api.d.ts.map +1 -0
- package/lib/host-api.js +40 -0
- package/lib/host-api.js.map +1 -0
- package/lib/index.d.ts +32 -0
- package/lib/index.d.ts.map +1 -0
- package/lib/index.js +594 -0
- package/lib/index.js.map +1 -0
- package/lib/scope-context.d.ts +3 -0
- package/lib/scope-context.d.ts.map +1 -0
- package/lib/scope-context.js +29 -0
- package/lib/scope-context.js.map +1 -0
- package/lib/scope-delegation.d.ts +23 -0
- package/lib/scope-delegation.d.ts.map +1 -0
- package/lib/scope-delegation.js +49 -0
- package/lib/scope-delegation.js.map +1 -0
- package/lib/scope-fs.d.ts +47 -0
- package/lib/scope-fs.d.ts.map +1 -0
- package/lib/scope-fs.js +150 -0
- package/lib/scope-fs.js.map +1 -0
- package/lib/scope-processes.d.ts +31 -0
- package/lib/scope-processes.d.ts.map +1 -0
- package/lib/scope-processes.js +71 -0
- package/lib/scope-processes.js.map +1 -0
- package/lib/scope-sandbox-linux.d.ts +35 -0
- package/lib/scope-sandbox-linux.d.ts.map +1 -0
- package/lib/scope-sandbox-linux.js +177 -0
- package/lib/scope-sandbox-linux.js.map +1 -0
- package/lib/scope-visibility.d.ts +14 -0
- package/lib/scope-visibility.d.ts.map +1 -0
- package/lib/scope-visibility.js +62 -0
- package/lib/scope-visibility.js.map +1 -0
- package/lib/session-scope.d.ts +54 -0
- package/lib/session-scope.d.ts.map +1 -0
- package/lib/session-scope.js +200 -0
- package/lib/session-scope.js.map +1 -0
- package/lib/tool-guard.d.ts +52 -0
- package/lib/tool-guard.d.ts.map +1 -0
- package/lib/tool-guard.js +233 -0
- package/lib/tool-guard.js.map +1 -0
- package/package.json +150 -0
package/lib/client.js
ADDED
|
@@ -0,0 +1,716 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// @ts-nocheck -- transitional port of the upstream module-loader client.
|
|
3
|
+
// dsh-session-scope — web client half (module-loader bundle).
|
|
4
|
+
//
|
|
5
|
+
// This file is the package's prebuilt client bundle: it registers one module
|
|
6
|
+
// with the web shell's module loader (window.__ModuleLoader__) and exports a
|
|
7
|
+
// Cordis client plugin. It requires only `react` and `react-dom` (both are
|
|
8
|
+
// shell statics); everything else comes from client services (`slots`,
|
|
9
|
+
// `connection`, `remote`, `sessions`).
|
|
10
|
+
//
|
|
11
|
+
// It contributes an independent Scope chip beside the Workspace picker while
|
|
12
|
+
// a session is blank and beside the permission selector after the first turn.
|
|
13
|
+
// The editor consumes the `session-scope` projection and writes
|
|
14
|
+
// complete snapshots through `/scope`; permission and scope never share UI
|
|
15
|
+
// state.
|
|
16
|
+
// - The editor walks the directory tree (breadcrumbs up to the filesystem
|
|
17
|
+
// root) and toggles which directories the agent may write to, in addition
|
|
18
|
+
// to the session workspace. Changes go through the host's
|
|
19
|
+
// `/scope` command; the `session-scope` session projection
|
|
20
|
+
// pushes the state back, so the button and the tree stay in sync with the
|
|
21
|
+
// server.
|
|
22
|
+
//
|
|
23
|
+
// The editor renders through a React portal onto document.body with a high
|
|
24
|
+
// z-index, because the composer seat is `position: sticky` inside its own
|
|
25
|
+
// stacking context — an in-place fixed overlay would be clipped or buried.
|
|
26
|
+
//
|
|
27
|
+
// Directory listings come from the host's `host.listDirectory` RPC (the
|
|
28
|
+
// browse capability) when the composition serves it; when the composition
|
|
29
|
+
// serves the native picker instead, the tree falls back to the plugin's own
|
|
30
|
+
// `/scope list` host command.
|
|
31
|
+
window.__ModuleLoader__.load({
|
|
32
|
+
id: 'dsh-session-scope',
|
|
33
|
+
factory: (require) => {
|
|
34
|
+
var module = { exports: {} };
|
|
35
|
+
var exports = module.exports;
|
|
36
|
+
Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
|
|
37
|
+
var React = require('react');
|
|
38
|
+
var ReactDOM = require('react-dom');
|
|
39
|
+
// ---------- copy ----------
|
|
40
|
+
var LANG = typeof navigator !== 'undefined' && /^zh/i.test(navigator.language || '') ? 'zh' : 'en';
|
|
41
|
+
function L(zh, en) {
|
|
42
|
+
return LANG === 'zh' ? zh : en;
|
|
43
|
+
}
|
|
44
|
+
// ---------- styling (cosmetic; never fail the plugin) ----------
|
|
45
|
+
var CSS = [
|
|
46
|
+
'.wss-btnScope { box-sizing: border-box; height: 22px; display: inline-flex; align-items: center; gap: 4px; border: none; border-radius: 6px; background: var(--dsw-alias-fill-tsp-secondary); color: var(--dsw-alias-label-secondary); padding: 0 8px; font: inherit; font-size: 12px; line-height: 22px; cursor: pointer; white-space: nowrap; }',
|
|
47
|
+
'.wss-btnScope:hover:not(:disabled) { background: var(--dsw-alias-interactive-bg-hover); color: var(--dsw-alias-label-primary); }',
|
|
48
|
+
'.wss-btnScope:disabled { cursor: default; opacity: .55; }',
|
|
49
|
+
'.wss-btnScopeHero { height: 28px; min-height: 28px; border-radius: 16px; background: transparent; color: var(--dsw-alias-label-primary); font-size: 13px; line-height: 20px; font-weight: 500; }',
|
|
50
|
+
'.wss-heroMount { display: contents; }',
|
|
51
|
+
'.wss-heroProbe { display: none !important; }',
|
|
52
|
+
'.wss-overlay { position: fixed; inset: 0; z-index: 1000; display: flex; align-items: center; justify-content: center; background: rgba(0,0,0,.45); }',
|
|
53
|
+
'.wss-modal { box-sizing: border-box; width: min(560px, calc(100vw - 48px)); max-height: min(640px, calc(100vh - 96px)); display: flex; flex-direction: column; gap: 10px; border: 1px solid var(--dsw-alias-border-inverted); background: var(--dsw-specific-menu); box-shadow: var(--dsw-shadow-lv3); border-radius: 14px; padding: 14px; color: var(--dsw-alias-label-primary); }',
|
|
54
|
+
'.wss-head { display: flex; align-items: center; justify-content: space-between; gap: 8px; }',
|
|
55
|
+
'.wss-title { font-size: 14px; font-weight: 600; line-height: 20px; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }',
|
|
56
|
+
'.wss-close { display: inline-flex; border: none; background: transparent; color: var(--dsw-alias-label-secondary); cursor: pointer; padding: 2px; border-radius: 6px; }',
|
|
57
|
+
'.wss-close:hover { background: var(--dsw-alias-interactive-bg-hover); }',
|
|
58
|
+
'.wss-caption { color: var(--dsw-alias-label-tertiary); font-size: 11px; line-height: 1.5; }',
|
|
59
|
+
'.wss-modes { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 6px; }',
|
|
60
|
+
'.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
|
+
'.wss-mode:hover:not(:disabled) { background: var(--dsw-alias-interactive-bg-hover); }',
|
|
62
|
+
'.wss-modeOn { border-color: var(--dsw-alias-state-business-primary); color: var(--dsw-alias-label-primary); background: var(--dsw-alias-fill-tsp-secondary); }',
|
|
63
|
+
'.wss-crumbs { display: flex; align-items: center; gap: 2px; flex-wrap: wrap; font-size: 12px; line-height: 18px; }',
|
|
64
|
+
'.wss-crumb { border: none; background: transparent; color: var(--dsw-alias-label-secondary); cursor: pointer; padding: 1px 4px; border-radius: 6px; font: inherit; }',
|
|
65
|
+
'.wss-crumb:hover { background: var(--dsw-alias-interactive-bg-hover); color: var(--dsw-alias-label-primary); }',
|
|
66
|
+
'.wss-crumbSep { color: var(--dsw-alias-label-caption); }',
|
|
67
|
+
'.wss-tree { display: flex; flex-direction: column; gap: 2px; overflow-y: auto; min-height: 120px; --dsh-scrollbar-thumb: var(--dsw-alias-scrollbar-bg-l2); --dsh-scrollbar-thumb-hover: var(--dsw-alias-scrollbar-hover-l2); }',
|
|
68
|
+
'.wss-row { box-sizing: border-box; display: flex; align-items: center; gap: 8px; width: 100%; text-align: left; border: none; background: transparent; color: var(--dsw-alias-label-primary); border-radius: 8px; padding: 5px 8px; cursor: pointer; font: inherit; }',
|
|
69
|
+
'.wss-row:hover { background: var(--dsw-alias-interactive-bg-hover); }',
|
|
70
|
+
'.wss-rowName { display: inline-flex; align-items: center; gap: 6px; min-width: 0; flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }',
|
|
71
|
+
'.wss-rowNameDim { opacity: .55; }',
|
|
72
|
+
'.wss-check { flex: none; display: inline-flex; align-items: center; justify-content: center; width: 18px; height: 18px; border-radius: 5px; border: 1px solid var(--dsw-alias-border-l2); background: transparent; color: #fff; cursor: pointer; padding: 0; }',
|
|
73
|
+
'.wss-check:hover:not(:disabled) { border-color: var(--dsw-alias-state-business-primary); }',
|
|
74
|
+
'.wss-checkOn { background: var(--dsw-alias-state-business-primary); border-color: var(--dsw-alias-state-business-primary); }',
|
|
75
|
+
'.wss-check:disabled { cursor: default; opacity: .6; }',
|
|
76
|
+
'.wss-hint { color: var(--dsw-alias-label-caption); font-size: 10px; line-height: 14px; white-space: nowrap; }',
|
|
77
|
+
'.wss-chevron { flex: none; display: inline-flex; color: var(--dsw-alias-label-caption); }',
|
|
78
|
+
'.wss-foot { display: flex; align-items: center; gap: 8px; border-top: 1px solid var(--dsw-alias-border-l2); padding-top: 10px; }',
|
|
79
|
+
'.wss-footRoots { min-width: 0; flex: 1; color: var(--dsw-alias-label-tertiary); font-size: 11px; line-height: 1.5; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }',
|
|
80
|
+
'.wss-btn { border: none; border-radius: 8px; padding: 4px 12px; font: inherit; font-size: 12px; line-height: 20px; cursor: pointer; white-space: nowrap; }',
|
|
81
|
+
'.wss-btn:disabled { opacity: .5; cursor: default; }',
|
|
82
|
+
'.wss-btnGhost { background: var(--dsw-alias-interactive-bg-hover); color: var(--dsw-alias-label-primary); }',
|
|
83
|
+
'.wss-btnPrimary { background: var(--dsw-alias-button-info-fill); color: #fff; }',
|
|
84
|
+
'.wss-btnDanger { background: transparent; color: var(--dsw-alias-state-error-primary); }',
|
|
85
|
+
'.wss-error { color: var(--dsw-alias-state-error-primary); font-size: 12px; line-height: 1.5; }',
|
|
86
|
+
'.wss-busy { color: var(--dsw-alias-label-tertiary); font-size: 12px; line-height: 1.5; }',
|
|
87
|
+
'.wss-empty { color: var(--dsw-alias-label-caption); font-size: 12px; line-height: 1.5; padding: 8px; }',
|
|
88
|
+
].join('\n');
|
|
89
|
+
// ---------- icons ----------
|
|
90
|
+
function IconFolder() {
|
|
91
|
+
return React.createElement('svg', { width: 14, height: 14, viewBox: '0 0 16 16', fill: 'none', 'aria-hidden': true }, React.createElement('path', { d: 'M2 3.5h4l1.5 2H14v7H2v-9Z', stroke: 'currentColor', strokeWidth: 1.2, strokeLinejoin: 'round' }));
|
|
92
|
+
}
|
|
93
|
+
function IconChevron() {
|
|
94
|
+
return React.createElement('svg', { width: 12, height: 12, viewBox: '0 0 12 12', fill: 'none', 'aria-hidden': true }, React.createElement('path', { d: 'M3 4.5L6 7.5L9 4.5', stroke: 'currentColor', strokeWidth: 1.5, strokeLinecap: 'round', strokeLinejoin: 'round' }));
|
|
95
|
+
}
|
|
96
|
+
function IconCheck() {
|
|
97
|
+
return React.createElement('svg', { width: 10, height: 10, viewBox: '0 0 12 12', fill: 'none', 'aria-hidden': true }, React.createElement('path', { d: 'M2.5 6.5L5 9L9.5 3.5', stroke: 'currentColor', strokeWidth: 2, strokeLinecap: 'round', strokeLinejoin: 'round' }));
|
|
98
|
+
}
|
|
99
|
+
function IconClose() {
|
|
100
|
+
return React.createElement('svg', { width: 12, height: 12, viewBox: '0 0 12 12', fill: 'none', 'aria-hidden': true }, React.createElement('path', { d: 'M3 3L9 9M9 3L3 9', stroke: 'currentColor', strokeWidth: 1.5, strokeLinecap: 'round' }));
|
|
101
|
+
}
|
|
102
|
+
function IconScope() {
|
|
103
|
+
return React.createElement('svg', { width: 14, height: 14, viewBox: '0 0 16 16', fill: 'none', 'aria-hidden': true }, React.createElement('path', { d: 'M8 1.5L14.5 4V8.5C14.5 12 11.6 14.2 8 15C4.4 14.2 1.5 12 1.5 8.5V4L8 1.5Z', stroke: 'currentColor', strokeWidth: 1.2, strokeLinejoin: 'round' }), React.createElement('path', { d: 'M8 8.5m-2 0a2 2 0 1 0 4 0a2 2 0 1 0-4 0', stroke: 'currentColor', strokeWidth: 1.1 }));
|
|
104
|
+
}
|
|
105
|
+
// ---------- shared helpers ----------
|
|
106
|
+
function sepOf(path) {
|
|
107
|
+
return path.indexOf('\\') !== -1 ? '\\' : '/';
|
|
108
|
+
}
|
|
109
|
+
// Whether `path` is `root` or lies beneath it (separator-aware prefix).
|
|
110
|
+
function isUnder(path, root) {
|
|
111
|
+
if (path === root)
|
|
112
|
+
return true;
|
|
113
|
+
var sep = sepOf(root);
|
|
114
|
+
var prefix = root.endsWith(sep) ? root : root + sep;
|
|
115
|
+
return path.indexOf(prefix) === 0;
|
|
116
|
+
}
|
|
117
|
+
// The deepest selected root that covers `path`, or undefined.
|
|
118
|
+
function coveringRoot(path, roots) {
|
|
119
|
+
var best = undefined;
|
|
120
|
+
for (var i = 0; i < roots.length; i++) {
|
|
121
|
+
var root = roots[i];
|
|
122
|
+
if (isUnder(path, root) && (best === undefined || root.length > best.length))
|
|
123
|
+
best = root;
|
|
124
|
+
}
|
|
125
|
+
return best;
|
|
126
|
+
}
|
|
127
|
+
function baseName(path) {
|
|
128
|
+
var sep = sepOf(path);
|
|
129
|
+
var parts = path.split(sep).filter(Boolean);
|
|
130
|
+
return parts.length === 0 ? path : parts[parts.length - 1];
|
|
131
|
+
}
|
|
132
|
+
function normalizeDraftRoots(roots) {
|
|
133
|
+
return Array.isArray(roots) ? roots.filter(function (root) { return typeof root === 'string'; }) : [];
|
|
134
|
+
}
|
|
135
|
+
function apply(ctx) {
|
|
136
|
+
var styleTag = null;
|
|
137
|
+
try {
|
|
138
|
+
styleTag = document.createElement('style');
|
|
139
|
+
styleTag.textContent = CSS;
|
|
140
|
+
document.head.appendChild(styleTag);
|
|
141
|
+
}
|
|
142
|
+
catch (err) { /* styling is cosmetic */ }
|
|
143
|
+
function connection() {
|
|
144
|
+
var value = ctx.get('connection');
|
|
145
|
+
return value !== undefined && value !== null ? value : undefined;
|
|
146
|
+
}
|
|
147
|
+
function remote() {
|
|
148
|
+
var value = ctx.get('remote');
|
|
149
|
+
return value !== undefined && value !== null ? value : undefined;
|
|
150
|
+
}
|
|
151
|
+
function api() {
|
|
152
|
+
var conn = connection();
|
|
153
|
+
return conn !== undefined && conn.api !== undefined ? conn.api : undefined;
|
|
154
|
+
}
|
|
155
|
+
// Execute one slash-command and return { ok, result } where result is
|
|
156
|
+
// the normalized { kind, text } command result when the host answered.
|
|
157
|
+
async function runCommand(sessionId, line) {
|
|
158
|
+
var rem = remote();
|
|
159
|
+
if (rem === undefined || typeof rem.commands === 'undefined' || typeof rem.commands.execute !== 'function') {
|
|
160
|
+
return { ok: false, error: 'remote command service unavailable' };
|
|
161
|
+
}
|
|
162
|
+
try {
|
|
163
|
+
// commands/execute carries an image list even when the command is
|
|
164
|
+
// text-only. Current DSH validates the generated remote arity.
|
|
165
|
+
var response = await rem.commands.execute(sessionId, line, []);
|
|
166
|
+
if (response === undefined || response === null || response.ok !== true) {
|
|
167
|
+
var message = response !== undefined && response !== null && response.error !== undefined && response.error.message !== undefined
|
|
168
|
+
? response.error.message
|
|
169
|
+
: 'command failed';
|
|
170
|
+
return { ok: false, error: message };
|
|
171
|
+
}
|
|
172
|
+
var result = response.value !== undefined && response.value !== null ? response.value.result : undefined;
|
|
173
|
+
if (result === undefined || result.kind !== 'success') {
|
|
174
|
+
return { ok: false, error: result !== undefined && result.text !== undefined ? result.text : 'command failed' };
|
|
175
|
+
}
|
|
176
|
+
return { ok: true, result: result };
|
|
177
|
+
}
|
|
178
|
+
catch (err) {
|
|
179
|
+
return { ok: false, error: err instanceof Error ? err.message : String(err) };
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
// List one directory level. Primary: host.listDirectory (browse
|
|
183
|
+
// capability). Fallback: the plugin's own /scope list
|
|
184
|
+
// command, used when the composition serves the native picker.
|
|
185
|
+
async function listLevel(sessionId, path) {
|
|
186
|
+
var face = api();
|
|
187
|
+
if (face !== undefined && face.host !== undefined && typeof face.host.listDirectory === 'function') {
|
|
188
|
+
try {
|
|
189
|
+
var response = await face.host.listDirectory({ path: path });
|
|
190
|
+
if (response !== undefined && response.result !== undefined && response.result.ok === true) {
|
|
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
|
+
}
|
|
204
|
+
}
|
|
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
|
+
try {
|
|
212
|
+
return { ok: true, value: JSON.parse(outcome.result.text), source: 'command' };
|
|
213
|
+
}
|
|
214
|
+
catch (err) {
|
|
215
|
+
return { ok: false, error: 'session-scope: host returned an invalid listing' };
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
// ---------- the scope editor (modal with the directory tree) ----------
|
|
219
|
+
function ScopeEditor(props) {
|
|
220
|
+
// props: sessionId, workspaceRoot (injected cwd, may be undefined),
|
|
221
|
+
// projectedRoot, scopeMode, scopeRoots, onClose
|
|
222
|
+
var state = React.useState({
|
|
223
|
+
root: null,
|
|
224
|
+
rootSource: null, // 'injected' | 'projection' | 'info'
|
|
225
|
+
path: null,
|
|
226
|
+
listing: null, // { path, crumbs, entries, truncated }
|
|
227
|
+
loading: false,
|
|
228
|
+
saving: false,
|
|
229
|
+
error: null,
|
|
230
|
+
phase: L('正在解析工作区…', 'Resolving workspace…'),
|
|
231
|
+
retryToken: 0,
|
|
232
|
+
isolatedSupported: null,
|
|
233
|
+
mode: props.scopeMode === 'focused' || props.scopeMode === 'isolated' ? props.scopeMode : 'full',
|
|
234
|
+
// Local pending content roots. In full mode the workspace root is
|
|
235
|
+
// inserted after root resolution to keep the checkbox semantics.
|
|
236
|
+
draft: normalizeDraftRoots(props.scopeRoots),
|
|
237
|
+
});
|
|
238
|
+
var snap = state[0];
|
|
239
|
+
var setSnap = state[1];
|
|
240
|
+
var patch = function (part) { setSnap(function (prev) { return Object.assign({}, prev, part); }); };
|
|
241
|
+
React.useEffect(function () {
|
|
242
|
+
var cancelled = false;
|
|
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`.
|
|
256
|
+
function applyRoot(root, source, mode, roots) {
|
|
257
|
+
var nextMode = mode === 'focused' || mode === 'isolated' ? mode : 'full';
|
|
258
|
+
patch({
|
|
259
|
+
root: root,
|
|
260
|
+
rootSource: source,
|
|
261
|
+
phase: L('正在加载目录…', 'Loading directories…'),
|
|
262
|
+
mode: nextMode,
|
|
263
|
+
draft: nextMode === 'full' ? [root] : normalizeDraftRoots(roots),
|
|
264
|
+
});
|
|
265
|
+
}
|
|
266
|
+
React.useEffect(function () {
|
|
267
|
+
var cancelled = false;
|
|
268
|
+
var timer = null;
|
|
269
|
+
(async function () {
|
|
270
|
+
try {
|
|
271
|
+
if (snap.root !== null)
|
|
272
|
+
return;
|
|
273
|
+
var injected = props.workspaceRoot;
|
|
274
|
+
if (injected !== undefined && injected !== null && injected !== '') {
|
|
275
|
+
applyRoot(injected, 'injected', snap.mode, snap.draft);
|
|
276
|
+
return;
|
|
277
|
+
}
|
|
278
|
+
var projected = props.projectedRoot;
|
|
279
|
+
if (projected !== undefined && projected !== null && projected !== '') {
|
|
280
|
+
applyRoot(projected, 'projection', snap.mode, snap.draft);
|
|
281
|
+
return;
|
|
282
|
+
}
|
|
283
|
+
patch({ phase: L('正在解析工作区…', 'Resolving workspace…') });
|
|
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);
|
|
311
|
+
}
|
|
312
|
+
catch (err) {
|
|
313
|
+
if (cancelled)
|
|
314
|
+
return;
|
|
315
|
+
patch({ loading: false, error: L('解析工作区失败:', 'failed to resolve the workspace: ') + (err instanceof Error ? err.message : String(err)), phase: null });
|
|
316
|
+
}
|
|
317
|
+
})();
|
|
318
|
+
return function () {
|
|
319
|
+
cancelled = true;
|
|
320
|
+
if (timer !== null)
|
|
321
|
+
clearTimeout(timer);
|
|
322
|
+
};
|
|
323
|
+
}, [snap.root, snap.retryToken, props.workspaceRoot, props.projectedRoot]);
|
|
324
|
+
// Load the first level when the root is known.
|
|
325
|
+
React.useEffect(function () {
|
|
326
|
+
var cancelled = false;
|
|
327
|
+
var timer = null;
|
|
328
|
+
if (snap.root === null || snap.path !== null)
|
|
329
|
+
return;
|
|
330
|
+
(async function () {
|
|
331
|
+
patch({ loading: true, error: null, phase: L('正在加载目录…', 'Loading directories…') });
|
|
332
|
+
timer = setTimeout(function () {
|
|
333
|
+
if (cancelled || snap.path !== null)
|
|
334
|
+
return;
|
|
335
|
+
patch({ loading: false, error: L('加载目录超时 — 请重试', 'loading the directory timed out — please retry'), phase: null });
|
|
336
|
+
}, 12000);
|
|
337
|
+
var outcome = await listLevel(props.sessionId, snap.root);
|
|
338
|
+
if (cancelled)
|
|
339
|
+
return;
|
|
340
|
+
if (timer !== null) {
|
|
341
|
+
clearTimeout(timer);
|
|
342
|
+
timer = null;
|
|
343
|
+
}
|
|
344
|
+
if (!outcome.ok) {
|
|
345
|
+
patch({ loading: false, error: outcome.error, phase: null });
|
|
346
|
+
return;
|
|
347
|
+
}
|
|
348
|
+
patch({ loading: false, path: outcome.value.path, listing: outcome.value, source: outcome.source, phase: null });
|
|
349
|
+
})();
|
|
350
|
+
return function () {
|
|
351
|
+
cancelled = true;
|
|
352
|
+
if (timer !== null)
|
|
353
|
+
clearTimeout(timer);
|
|
354
|
+
};
|
|
355
|
+
}, [snap.root, snap.path]);
|
|
356
|
+
// Retry from scratch after a failure.
|
|
357
|
+
function retry() {
|
|
358
|
+
patch({
|
|
359
|
+
root: null,
|
|
360
|
+
path: null,
|
|
361
|
+
listing: null,
|
|
362
|
+
loading: false,
|
|
363
|
+
error: null,
|
|
364
|
+
phase: L('正在解析工作区…', 'Resolving workspace…'),
|
|
365
|
+
retryToken: snap.retryToken + 1,
|
|
366
|
+
});
|
|
367
|
+
}
|
|
368
|
+
// Navigate into a directory.
|
|
369
|
+
function enter(path) {
|
|
370
|
+
patch({ loading: true, error: null, phase: L('正在加载目录…', 'Loading directories…') });
|
|
371
|
+
var settled = false;
|
|
372
|
+
var timer = setTimeout(function () {
|
|
373
|
+
if (settled)
|
|
374
|
+
return;
|
|
375
|
+
settled = true;
|
|
376
|
+
patch({ loading: false, error: L('加载目录超时 — 请重试', 'loading the directory timed out — please retry'), phase: null });
|
|
377
|
+
}, 12000);
|
|
378
|
+
listLevel(props.sessionId, path).then(function (outcome) {
|
|
379
|
+
if (settled)
|
|
380
|
+
return;
|
|
381
|
+
settled = true;
|
|
382
|
+
clearTimeout(timer);
|
|
383
|
+
if (!outcome.ok) {
|
|
384
|
+
patch({ loading: false, error: outcome.error, phase: null });
|
|
385
|
+
return;
|
|
386
|
+
}
|
|
387
|
+
patch({ loading: false, path: outcome.value.path, listing: outcome.value, source: outcome.source, phase: null });
|
|
388
|
+
}, function (err) {
|
|
389
|
+
if (settled)
|
|
390
|
+
return;
|
|
391
|
+
settled = true;
|
|
392
|
+
clearTimeout(timer);
|
|
393
|
+
console.error('[session-scope] directory listing failed:', err);
|
|
394
|
+
patch({ loading: false, error: L('加载目录失败:', 'failed to load the directory: ') + (err instanceof Error ? err.message : String(err)), phase: null });
|
|
395
|
+
});
|
|
396
|
+
}
|
|
397
|
+
// Persist the whole pending draft. Called once from the Done button,
|
|
398
|
+
// never per toggle. The RPC must not leave the modal stuck: a timeout
|
|
399
|
+
// and a rejection handler both settle the flag and surface a visible
|
|
400
|
+
// error, keeping the modal open with the draft intact.
|
|
401
|
+
function save(mode, draft) {
|
|
402
|
+
patch({ saving: true, error: null });
|
|
403
|
+
var settled = false;
|
|
404
|
+
var timer = setTimeout(function () {
|
|
405
|
+
if (settled)
|
|
406
|
+
return;
|
|
407
|
+
settled = true;
|
|
408
|
+
patch({ saving: false, error: L('保存超时 — 请重试', 'saving timed out — please retry') });
|
|
409
|
+
}, 12000);
|
|
410
|
+
var effectiveMode = snap.root !== null && draft.indexOf(snap.root) !== -1 ? 'full' : mode;
|
|
411
|
+
var command = effectiveMode === 'full'
|
|
412
|
+
? '/scope full'
|
|
413
|
+
: '/scope ' + effectiveMode + ' ' + JSON.stringify(draft);
|
|
414
|
+
runCommand(props.sessionId, command).then(function (outcome) {
|
|
415
|
+
if (settled)
|
|
416
|
+
return;
|
|
417
|
+
settled = true;
|
|
418
|
+
clearTimeout(timer);
|
|
419
|
+
if (!outcome.ok) {
|
|
420
|
+
patch({ saving: false, error: outcome.error });
|
|
421
|
+
return;
|
|
422
|
+
}
|
|
423
|
+
patch({ saving: false });
|
|
424
|
+
props.onClose();
|
|
425
|
+
}, function (err) {
|
|
426
|
+
if (settled)
|
|
427
|
+
return;
|
|
428
|
+
settled = true;
|
|
429
|
+
clearTimeout(timer);
|
|
430
|
+
var message = err instanceof Error ? err.message : String(err);
|
|
431
|
+
console.error('[session-scope] save command failed:', err);
|
|
432
|
+
patch({ saving: false, error: L('保存失败:', 'save failed: ') + message });
|
|
433
|
+
});
|
|
434
|
+
}
|
|
435
|
+
// Toggle one directory in the LOCAL draft (no RPC yet — the draft is
|
|
436
|
+
// persisted on Done). The workspace root is an ordinary member: it is
|
|
437
|
+
// toggled exactly like every other row, so the checked state always
|
|
438
|
+
// matches the visible content roots. A row that is only COVERED by a selected
|
|
439
|
+
// ancestor is not toggleable here — uncheck the ancestor (its row
|
|
440
|
+
// shows the covering check) to stop including the whole subtree.
|
|
441
|
+
function toggle(path) {
|
|
442
|
+
var self = snap.draft.indexOf(path);
|
|
443
|
+
var next;
|
|
444
|
+
if (self !== -1) {
|
|
445
|
+
next = snap.draft.filter(function (root) { return root !== path; });
|
|
446
|
+
}
|
|
447
|
+
else {
|
|
448
|
+
next = snap.draft.concat([path]);
|
|
449
|
+
}
|
|
450
|
+
if (snap.root !== null && path === snap.root) {
|
|
451
|
+
patch({
|
|
452
|
+
mode: self === -1 ? 'full' : (snap.mode === 'isolated' ? 'isolated' : 'focused'),
|
|
453
|
+
draft: self === -1 ? [snap.root] : next,
|
|
454
|
+
error: null,
|
|
455
|
+
});
|
|
456
|
+
return;
|
|
457
|
+
}
|
|
458
|
+
patch({ draft: next, error: null });
|
|
459
|
+
}
|
|
460
|
+
function selectMode(mode) {
|
|
461
|
+
if (mode === 'isolated' && snap.isolatedSupported === false)
|
|
462
|
+
return;
|
|
463
|
+
if (mode === 'full') {
|
|
464
|
+
patch({ mode: 'full', draft: snap.root === null ? [] : [snap.root], error: null });
|
|
465
|
+
return;
|
|
466
|
+
}
|
|
467
|
+
patch({
|
|
468
|
+
mode: mode,
|
|
469
|
+
draft: snap.root === null ? snap.draft : snap.draft.filter(function (root) { return root !== snap.root; }),
|
|
470
|
+
error: null,
|
|
471
|
+
});
|
|
472
|
+
}
|
|
473
|
+
// Escape / outside-click closes the modal.
|
|
474
|
+
var modalRef = React.useRef(null);
|
|
475
|
+
React.useEffect(function () {
|
|
476
|
+
function onDown(ev) {
|
|
477
|
+
if (modalRef.current !== null && ev.target instanceof Node && modalRef.current.contains(ev.target))
|
|
478
|
+
return;
|
|
479
|
+
props.onClose();
|
|
480
|
+
}
|
|
481
|
+
function onKey(ev) {
|
|
482
|
+
if (ev.key !== 'Escape')
|
|
483
|
+
return;
|
|
484
|
+
ev.preventDefault();
|
|
485
|
+
ev.stopPropagation();
|
|
486
|
+
props.onClose();
|
|
487
|
+
}
|
|
488
|
+
document.addEventListener('pointerdown', onDown, true);
|
|
489
|
+
document.addEventListener('keydown', onKey, true);
|
|
490
|
+
return function () {
|
|
491
|
+
document.removeEventListener('pointerdown', onDown, true);
|
|
492
|
+
document.removeEventListener('keydown', onKey, true);
|
|
493
|
+
};
|
|
494
|
+
}, []);
|
|
495
|
+
var listing = snap.listing;
|
|
496
|
+
var crumbs = listing !== null ? listing.crumbs : [];
|
|
497
|
+
var entries = listing !== null ? listing.entries : [];
|
|
498
|
+
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
|
+
{ value: 'full', label: L('整个工作区', 'Entire workspace') },
|
|
500
|
+
{ value: 'focused', label: L('聚焦', 'Focused') },
|
|
501
|
+
{ value: 'isolated', label: L('隔离', 'Isolated') },
|
|
502
|
+
].map(function (option) {
|
|
503
|
+
var unavailable = option.value === 'isolated' && snap.isolatedSupported === false;
|
|
504
|
+
return React.createElement('button', {
|
|
505
|
+
key: option.value,
|
|
506
|
+
type: 'button',
|
|
507
|
+
role: 'radio',
|
|
508
|
+
'aria-checked': snap.mode === option.value,
|
|
509
|
+
className: 'wss-mode' + (snap.mode === option.value ? ' wss-modeOn' : ''),
|
|
510
|
+
disabled: snap.saving || unavailable,
|
|
511
|
+
title: unavailable ? L('此主机不支持隔离模式', 'Isolated mode is unavailable on this host') : option.label,
|
|
512
|
+
onClick: function () { selectMode(option.value); },
|
|
513
|
+
}, option.label);
|
|
514
|
+
})), 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', {
|
|
515
|
+
type: 'button',
|
|
516
|
+
className: 'wss-btn wss-btnGhost',
|
|
517
|
+
onClick: retry,
|
|
518
|
+
style: { marginLeft: 8 },
|
|
519
|
+
}, L('重试', 'Retry'))), snap.root !== null && React.createElement('div', { className: 'wss-crumbs' }, crumbs.map(function (crumb, index) {
|
|
520
|
+
return React.createElement(React.Fragment, { key: crumb.path }, index > 0 && React.createElement('span', { className: 'wss-crumbSep' }, '/'), React.createElement('button', {
|
|
521
|
+
type: 'button',
|
|
522
|
+
className: 'wss-crumb',
|
|
523
|
+
title: crumb.path,
|
|
524
|
+
onClick: function () {
|
|
525
|
+
if (crumb.path !== snap.path)
|
|
526
|
+
enter(crumb.path);
|
|
527
|
+
},
|
|
528
|
+
}, crumb.name));
|
|
529
|
+
})), 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
|
+
type: 'button',
|
|
531
|
+
className: 'wss-check' + (coveringRoot(snap.path, snap.draft) !== undefined ? ' wss-checkOn' : ''),
|
|
532
|
+
disabled: snap.saving || (coveringRoot(snap.path, snap.draft) !== undefined && snap.draft.indexOf(snap.path) === -1),
|
|
533
|
+
'aria-label': L('切换目录', 'Toggle directory') + ' ' + snap.path,
|
|
534
|
+
title: coveringRoot(snap.path, snap.draft) !== undefined && snap.draft.indexOf(snap.path) === -1
|
|
535
|
+
? L('经父目录包含:取消父目录后整个子树将不可见', 'Included via a parent directory; uncheck the parent to hide its whole subtree')
|
|
536
|
+
: snap.path === snap.root
|
|
537
|
+
? (snap.draft.indexOf(snap.path) !== -1
|
|
538
|
+
? L('整个工作区可见 — 取消勾选后选择聚焦范围', 'The entire workspace is visible — uncheck to choose a focused scope')
|
|
539
|
+
: L('勾选以显示整个工作区', 'Check to expose the entire workspace'))
|
|
540
|
+
: snap.path,
|
|
541
|
+
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 &&
|
|
543
|
+
React.createElement('span', { className: 'wss-hint' }, L('经父目录包含', 'via parent')), snap.path === snap.root && coveringRoot(snap.path, snap.draft) === undefined &&
|
|
544
|
+
React.createElement('span', { className: 'wss-hint' }, L('聚焦范围', 'focused scope'))), !snap.loading && snap.path !== null && entries.map(function (entry) {
|
|
545
|
+
var covered = coveringRoot(entry.path, snap.draft);
|
|
546
|
+
var self = snap.draft.indexOf(entry.path) !== -1;
|
|
547
|
+
var on = covered !== undefined;
|
|
548
|
+
return React.createElement('div', {
|
|
549
|
+
key: entry.path,
|
|
550
|
+
className: 'wss-row',
|
|
551
|
+
onClick: function () { enter(entry.path); },
|
|
552
|
+
}, React.createElement('button', {
|
|
553
|
+
type: 'button',
|
|
554
|
+
className: 'wss-check' + (on ? ' wss-checkOn' : ''),
|
|
555
|
+
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,
|
|
558
|
+
onClick: function (ev) { ev.stopPropagation(); toggle(entry.path); },
|
|
559
|
+
}, 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
|
+
}), !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'
|
|
562
|
+
? L('整个工作区 · ', 'entire workspace · ')
|
|
563
|
+
: (snap.mode === 'isolated' ? L('隔离 · ', 'isolated · ') : L('聚焦 · ', 'focused · '))) +
|
|
564
|
+
(snap.draft.length === 0
|
|
565
|
+
? L('未选择目录', 'no directories selected')
|
|
566
|
+
: L('已选择 ' + snap.draft.length + ' 个目录', String(snap.draft.length) + ' director' + (snap.draft.length === 1 ? 'y' : 'ies') + ' selected'))), React.createElement('button', {
|
|
567
|
+
type: 'button',
|
|
568
|
+
className: 'wss-btn wss-btnDanger',
|
|
569
|
+
disabled: snap.saving || snap.draft.length === 0,
|
|
570
|
+
onClick: function () { patch({ mode: 'focused', draft: [], error: null }); },
|
|
571
|
+
}, L('清除全部', 'Clear all')), React.createElement('button', {
|
|
572
|
+
type: 'button',
|
|
573
|
+
className: 'wss-btn wss-btnPrimary',
|
|
574
|
+
disabled: snap.saving,
|
|
575
|
+
onClick: function () { save(snap.mode, snap.draft); },
|
|
576
|
+
}, L('应用', 'Apply'))), snap.saving && React.createElement('div', { className: 'wss-busy' }, L('保存中…', 'Saving…'))));
|
|
577
|
+
// The composer seat is sticky inside its own stacking context, so the
|
|
578
|
+
// editor is portaled to document.body with a high z-index.
|
|
579
|
+
return ReactDOM.createPortal(overlay, document.body);
|
|
580
|
+
}
|
|
581
|
+
// ---------- independent Scope chip ----------
|
|
582
|
+
function ScopeButton(props) {
|
|
583
|
+
// props: useProjection, sessionId, workspaceRoot (injected)
|
|
584
|
+
var scope = props.useProjection('session-scope');
|
|
585
|
+
var openState = React.useState(false);
|
|
586
|
+
var open = openState[0];
|
|
587
|
+
var setOpen = openState[1];
|
|
588
|
+
var heroMountState = React.useState(null);
|
|
589
|
+
var heroMount = heroMountState[0];
|
|
590
|
+
var setHeroMount = heroMountState[1];
|
|
591
|
+
var heroCheckedState = React.useState(false);
|
|
592
|
+
var heroChecked = heroCheckedState[0];
|
|
593
|
+
var setHeroChecked = heroCheckedState[1];
|
|
594
|
+
var heroProbe = React.useRef(null);
|
|
595
|
+
var blank = props.session !== undefined && props.session.composerPhase === 'blank';
|
|
596
|
+
React.useLayoutEffect(function () {
|
|
597
|
+
if (!blank) {
|
|
598
|
+
if (heroMount !== null)
|
|
599
|
+
setHeroMount(null);
|
|
600
|
+
if (heroChecked)
|
|
601
|
+
setHeroChecked(false);
|
|
602
|
+
return undefined;
|
|
603
|
+
}
|
|
604
|
+
var probe = heroProbe.current;
|
|
605
|
+
var heroRoot = probe !== null && typeof probe.closest === 'function'
|
|
606
|
+
? probe.closest('[data-phase="hero"]')
|
|
607
|
+
: null;
|
|
608
|
+
// Workspace is the first menu trigger in the hero tree. This uses
|
|
609
|
+
// semantic DOM already exposed by DSH, not localized copy or its
|
|
610
|
+
// generated CSS-module class names.
|
|
611
|
+
var workspaceButton = heroRoot !== null && typeof heroRoot.querySelector === 'function'
|
|
612
|
+
? heroRoot.querySelector('button[aria-haspopup="menu"]')
|
|
613
|
+
: null;
|
|
614
|
+
var row = workspaceButton !== null ? workspaceButton.parentNode : null;
|
|
615
|
+
if (row === null || typeof row.insertBefore !== 'function') {
|
|
616
|
+
setHeroChecked(true);
|
|
617
|
+
return undefined;
|
|
618
|
+
}
|
|
619
|
+
var mount = document.createElement('span');
|
|
620
|
+
mount.className = 'wss-heroMount';
|
|
621
|
+
mount.setAttribute('data-session-scope-hero-mount', '');
|
|
622
|
+
row.insertBefore(mount, workspaceButton.nextSibling);
|
|
623
|
+
setHeroMount(mount);
|
|
624
|
+
setHeroChecked(true);
|
|
625
|
+
return function () {
|
|
626
|
+
if (mount.parentNode !== null)
|
|
627
|
+
mount.parentNode.removeChild(mount);
|
|
628
|
+
};
|
|
629
|
+
}, [blank]);
|
|
630
|
+
var roots = scope !== undefined && Array.isArray(scope.roots) ? scope.roots : [];
|
|
631
|
+
var projectedRoot = scope !== undefined && typeof scope.workspaceRoot === 'string' && scope.workspaceRoot !== '' ? scope.workspaceRoot : undefined;
|
|
632
|
+
var mode = scope !== undefined && (scope.mode === 'focused' || scope.mode === 'isolated') ? scope.mode : 'full';
|
|
633
|
+
var label = mode === 'full'
|
|
634
|
+
? L('范围:全部', 'Scope: All')
|
|
635
|
+
: roots.length === 0
|
|
636
|
+
? L('范围:无', 'Scope: None')
|
|
637
|
+
: roots.length === 1
|
|
638
|
+
? L('范围:', 'Scope: ') + baseName(roots[0])
|
|
639
|
+
: L('范围:' + String(roots.length) + ' 个目录', 'Scope: ' + String(roots.length) + ' roots');
|
|
640
|
+
var title = mode === 'isolated'
|
|
641
|
+
? L('隔离会话范围', 'Isolated session scope')
|
|
642
|
+
: mode === 'focused' ? L('聚焦会话范围', 'Focused session scope') : L('整个工作区可见', 'Entire workspace visible');
|
|
643
|
+
var button = React.createElement('button', {
|
|
644
|
+
type: 'button',
|
|
645
|
+
className: blank && heroMount !== null ? 'wss-btnScope wss-btnScopeHero' : 'wss-btnScope',
|
|
646
|
+
'data-session-scope-button': true,
|
|
647
|
+
'aria-haspopup': 'dialog',
|
|
648
|
+
'aria-expanded': open,
|
|
649
|
+
'aria-label': label,
|
|
650
|
+
title: title,
|
|
651
|
+
onClick: function () { setOpen(true); },
|
|
652
|
+
}, React.createElement('span', { style: { display: 'inline-flex' } }, IconScope()), label, React.createElement('span', { className: 'wss-chevron' }, IconChevron()));
|
|
653
|
+
return React.createElement(React.Fragment, null, blank && React.createElement('span', { ref: heroProbe, className: 'wss-heroProbe', 'aria-hidden': true }), blank && heroMount !== null
|
|
654
|
+
? ReactDOM.createPortal(button, heroMount)
|
|
655
|
+
: (!blank || heroChecked) && button, open && React.createElement(ScopeEditor, {
|
|
656
|
+
sessionId: props.sessionId,
|
|
657
|
+
workspaceRoot: props.workspaceRoot,
|
|
658
|
+
projectedRoot: projectedRoot,
|
|
659
|
+
scopeMode: mode,
|
|
660
|
+
scopeRoots: roots,
|
|
661
|
+
onClose: function () { setOpen(false); },
|
|
662
|
+
}));
|
|
663
|
+
}
|
|
664
|
+
// ---------- registration ----------
|
|
665
|
+
var disposers = [];
|
|
666
|
+
var slots = ctx.get('slots');
|
|
667
|
+
if (slots !== undefined) {
|
|
668
|
+
function scopeInjection(sessionId) {
|
|
669
|
+
// The session's workspace root never changes; the sessions list
|
|
670
|
+
// store (byId, keyed by session id) is the cheapest reliable
|
|
671
|
+
// source. The editor falls back to the projection root and then to
|
|
672
|
+
// /scope show.
|
|
673
|
+
var root = undefined;
|
|
674
|
+
try {
|
|
675
|
+
var sessions = ctx.get('sessions');
|
|
676
|
+
if (sessions !== undefined && sessions.list !== undefined && typeof sessions.list.getSnapshot === 'function') {
|
|
677
|
+
var snapshot = sessions.list.getSnapshot();
|
|
678
|
+
var entry = snapshot.byId !== undefined ? snapshot.byId[sessionId] : undefined;
|
|
679
|
+
if (entry !== undefined && entry.cwd !== undefined)
|
|
680
|
+
root = entry.cwd;
|
|
681
|
+
}
|
|
682
|
+
}
|
|
683
|
+
catch (err) { /* non-fatal: the editor resolves the root itself */ }
|
|
684
|
+
return { workspaceRoot: root };
|
|
685
|
+
}
|
|
686
|
+
// This existing session-scoped seat supplies both the ordinary
|
|
687
|
+
// composer control and the lifecycle anchor for its blank-session
|
|
688
|
+
// hero portal.
|
|
689
|
+
disposers.push(ctx.effect(function () {
|
|
690
|
+
return slots.inject('conversation.input.left', function () {
|
|
691
|
+
return slots.register({
|
|
692
|
+
name: 'conversation.input.left',
|
|
693
|
+
id: 'session-scope',
|
|
694
|
+
order: 0,
|
|
695
|
+
inject: function (sessionId) { return scopeInjection(sessionId); },
|
|
696
|
+
}, ScopeButton);
|
|
697
|
+
});
|
|
698
|
+
}));
|
|
699
|
+
}
|
|
700
|
+
return function () {
|
|
701
|
+
for (var i = 0; i < disposers.length; i++) {
|
|
702
|
+
try {
|
|
703
|
+
disposers[i]();
|
|
704
|
+
}
|
|
705
|
+
catch (err) { /* best effort */ }
|
|
706
|
+
}
|
|
707
|
+
if (styleTag !== null && styleTag.parentNode !== null)
|
|
708
|
+
styleTag.parentNode.removeChild(styleTag);
|
|
709
|
+
};
|
|
710
|
+
}
|
|
711
|
+
exports.apply = apply;
|
|
712
|
+
exports.inject = ['slots', 'connection', 'remote', 'remote.commands', 'sessions'];
|
|
713
|
+
return module.exports;
|
|
714
|
+
},
|
|
715
|
+
});
|
|
716
|
+
//# sourceMappingURL=client.js.map
|