@bramblex/codex-workbench 0.1.4 → 0.1.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +5 -2
- package/package.json +1 -1
- package/src/cli-output.js +10 -3
- package/src/cli.js +1 -0
- package/src/services/session-sources.js +23 -4
- package/src/services/ssh-runner.js +75 -2
- package/src/ui/workbench.js +114 -13
package/README.md
CHANGED
|
@@ -40,7 +40,7 @@ codex-workbench list
|
|
|
40
40
|
```sh
|
|
41
41
|
codex-workbench [ui]
|
|
42
42
|
codex-workbench doctor
|
|
43
|
-
codex-workbench list [--json] [--cwd <dir>] [--all]
|
|
43
|
+
codex-workbench list [--json] [--compact] [--cwd <dir>] [--all]
|
|
44
44
|
codex-workbench show <session>
|
|
45
45
|
codex-workbench rename <session> <name>
|
|
46
46
|
codex-workbench note <session> <note>
|
|
@@ -73,10 +73,13 @@ Use `list` to find sessions:
|
|
|
73
73
|
```sh
|
|
74
74
|
codex-workbench list
|
|
75
75
|
codex-workbench list --json
|
|
76
|
+
codex-workbench list --json --compact
|
|
76
77
|
codex-workbench list --cwd /path/to/project
|
|
77
78
|
codex-workbench list --all
|
|
78
79
|
```
|
|
79
80
|
|
|
81
|
+
Use `--compact` with `--json` when another tool only needs session summaries. It omits the full message history and keeps remote SSH listings small.
|
|
82
|
+
|
|
80
83
|
Use `new` to start a fresh Codex session in a project directory:
|
|
81
84
|
|
|
82
85
|
```sh
|
|
@@ -108,7 +111,7 @@ Most commands accept a full session id, a unique prefix, a saved name, or a sess
|
|
|
108
111
|
|
|
109
112
|
## Interactive UI
|
|
110
113
|
|
|
111
|
-
The UI groups sessions by source and working directory, with sources/projects on the left, sessions on the upper right, and details below. When you start or resume a session, Codex temporarily takes over the terminal; when Codex exits, codex-workbench redraws the UI.
|
|
114
|
+
The UI groups sessions by source and working directory, with sources/projects on the left, sessions on the upper right, and details below. Local sessions render immediately, while remote SSH sources load in the background. When you start or resume a session, Codex temporarily takes over the terminal; when Codex exits, codex-workbench redraws the UI.
|
|
112
115
|
|
|
113
116
|
Common keys:
|
|
114
117
|
|
package/package.json
CHANGED
package/src/cli-output.js
CHANGED
|
@@ -10,7 +10,7 @@ function usage() {
|
|
|
10
10
|
Usage:
|
|
11
11
|
codex-workbench [ui]
|
|
12
12
|
codex-workbench doctor
|
|
13
|
-
codex-workbench list [--json] [--cwd <dir>] [--all]
|
|
13
|
+
codex-workbench list [--json] [--compact] [--cwd <dir>] [--all]
|
|
14
14
|
codex-workbench show <session>
|
|
15
15
|
codex-workbench rename <session> <name>
|
|
16
16
|
codex-workbench note <session> <note>
|
|
@@ -41,7 +41,8 @@ function printList(sessions, opts = {}) {
|
|
|
41
41
|
return true;
|
|
42
42
|
});
|
|
43
43
|
if (opts.json) {
|
|
44
|
-
|
|
44
|
+
const payload = opts.compact ? filtered.map(compactSession) : filtered;
|
|
45
|
+
console.log(JSON.stringify(payload, null, 2));
|
|
45
46
|
return;
|
|
46
47
|
}
|
|
47
48
|
const groups = new Map();
|
|
@@ -62,6 +63,11 @@ function printList(sessions, opts = {}) {
|
|
|
62
63
|
if (!filtered.length) console.log('No sessions found.');
|
|
63
64
|
}
|
|
64
65
|
|
|
66
|
+
function compactSession(session) {
|
|
67
|
+
const { messages, ...compact } = session;
|
|
68
|
+
return compact;
|
|
69
|
+
}
|
|
70
|
+
|
|
65
71
|
function printShow(session) {
|
|
66
72
|
console.log(`${session.name || '(unnamed)'} ${session.archived ? '[archived]' : ''}${session.hidden ? '[hidden]' : ''}`);
|
|
67
73
|
console.log(`id: ${session.id}`);
|
|
@@ -73,7 +79,7 @@ function printShow(session) {
|
|
|
73
79
|
console.log(`turns: ${session.turns}`);
|
|
74
80
|
if (session.note) console.log(`note: ${session.note}`);
|
|
75
81
|
console.log('\nMessages:');
|
|
76
|
-
for (const msg of session.messages) {
|
|
82
|
+
for (const msg of session.messages || []) {
|
|
77
83
|
if (msg.role === 'developer') continue;
|
|
78
84
|
const prefix = msg.role === 'assistant' ? 'A' : msg.role === 'user' ? 'U' : msg.role.slice(0, 1).toUpperCase();
|
|
79
85
|
console.log(` ${prefix}: ${truncate(msg.text, 180)}`);
|
|
@@ -102,6 +108,7 @@ function printDoctor() {
|
|
|
102
108
|
}
|
|
103
109
|
|
|
104
110
|
module.exports = {
|
|
111
|
+
compactSession,
|
|
105
112
|
printDoctor,
|
|
106
113
|
printList,
|
|
107
114
|
printShow,
|
package/src/cli.js
CHANGED
|
@@ -27,6 +27,7 @@ function parseFlags(args) {
|
|
|
27
27
|
const arg = args[i];
|
|
28
28
|
if (arg === '--json') out.json = true;
|
|
29
29
|
else if (arg === '--all') out.all = true;
|
|
30
|
+
else if (arg === '--compact') out.compact = true;
|
|
30
31
|
else if (arg === '--force') out.force = true;
|
|
31
32
|
else if (arg === '--file') out.file = true;
|
|
32
33
|
else if (arg === '--cwd') {
|
|
@@ -5,7 +5,7 @@ const { createChildDirectory, listDirectories } = require('../model/directories'
|
|
|
5
5
|
const { listSessions, updateMetadata } = require('../model/session-store');
|
|
6
6
|
const { listServers } = require('../model/workbench-config');
|
|
7
7
|
const { runCodexCommand, runNewCodexSession, usableCwd } = require('./codex-runner');
|
|
8
|
-
const { runRemoteCwb, runRemoteCwbJson } = require('./ssh-runner');
|
|
8
|
+
const { runRemoteCwb, runRemoteCwbJson, runRemoteCwbJsonAsync } = require('./ssh-runner');
|
|
9
9
|
|
|
10
10
|
const LOCAL_SOURCE = {
|
|
11
11
|
id: 'local',
|
|
@@ -41,14 +41,31 @@ function configuredSources() {
|
|
|
41
41
|
return [LOCAL_SOURCE, ...listServers().map(sourceForServer)];
|
|
42
42
|
}
|
|
43
43
|
|
|
44
|
+
function sortSessions(sessions) {
|
|
45
|
+
sessions.sort((a, b) => new Date(b.updatedAt) - new Date(a.updatedAt));
|
|
46
|
+
return sessions;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function loadLocalWorkbenchSessions(sources = configuredSources()) {
|
|
50
|
+
const sessions = listSessions().map((session) => attachSource(session, LOCAL_SOURCE));
|
|
51
|
+
return { errors: [], sessions, sources };
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function loadRemoteSourceSessions(source) {
|
|
55
|
+
return runRemoteCwbJsonAsync(source, ['list', '--json', '--compact']).then((remoteSessions) => {
|
|
56
|
+
if (!Array.isArray(remoteSessions)) throw new Error('remote list did not return an array');
|
|
57
|
+
return remoteSessions.map((session) => attachSource(session, source));
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
|
|
44
61
|
function loadWorkbenchSessions() {
|
|
45
62
|
const sources = configuredSources();
|
|
46
|
-
const sessions =
|
|
63
|
+
const sessions = loadLocalWorkbenchSessions(sources).sessions;
|
|
47
64
|
const errors = [];
|
|
48
65
|
|
|
49
66
|
for (const source of sources.filter((candidate) => candidate.remote)) {
|
|
50
67
|
try {
|
|
51
|
-
const remoteSessions = runRemoteCwbJson(source, ['list', '--json']);
|
|
68
|
+
const remoteSessions = runRemoteCwbJson(source, ['list', '--json', '--compact']);
|
|
52
69
|
if (!Array.isArray(remoteSessions)) throw new Error('remote list did not return an array');
|
|
53
70
|
sessions.push(...remoteSessions.map((session) => attachSource(session, source)));
|
|
54
71
|
} catch (err) {
|
|
@@ -56,7 +73,7 @@ function loadWorkbenchSessions() {
|
|
|
56
73
|
}
|
|
57
74
|
}
|
|
58
75
|
|
|
59
|
-
sessions
|
|
76
|
+
sortSessions(sessions);
|
|
60
77
|
return { errors, sessions, sources };
|
|
61
78
|
}
|
|
62
79
|
|
|
@@ -139,6 +156,8 @@ module.exports = {
|
|
|
139
156
|
configuredSources,
|
|
140
157
|
createSourceDirectory,
|
|
141
158
|
listSourceDirectories,
|
|
159
|
+
loadLocalWorkbenchSessions,
|
|
160
|
+
loadRemoteSourceSessions,
|
|
142
161
|
loadWorkbenchSessions,
|
|
143
162
|
runSourceNewSession,
|
|
144
163
|
runSourceSessionCommand,
|
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
-
const { spawnSync } = require('child_process');
|
|
3
|
+
const { spawn, spawnSync } = require('child_process');
|
|
4
4
|
const { shellQuote } = require('./codex-runner');
|
|
5
5
|
|
|
6
|
+
const DEFAULT_MAX_BUFFER = 64 * 1024 * 1024;
|
|
7
|
+
|
|
6
8
|
function sshBaseArgs(server, opts = {}) {
|
|
7
9
|
const args = [];
|
|
8
10
|
if (opts.tty) args.push('-t');
|
|
@@ -21,13 +23,82 @@ function runRemoteCwb(server, argv, opts = {}) {
|
|
|
21
23
|
return spawnSync('ssh', args, {
|
|
22
24
|
encoding: opts.encoding,
|
|
23
25
|
env: process.env,
|
|
26
|
+
maxBuffer: opts.maxBuffer || DEFAULT_MAX_BUFFER,
|
|
24
27
|
stdio: opts.stdio || (opts.encoding ? ['ignore', 'pipe', 'pipe'] : 'inherit'),
|
|
25
28
|
});
|
|
26
29
|
}
|
|
27
30
|
|
|
31
|
+
function runRemoteCwbAsync(server, argv, opts = {}) {
|
|
32
|
+
const command = remoteCwbCommand(server, argv);
|
|
33
|
+
const args = [...sshBaseArgs(server, { tty: opts.tty }), command];
|
|
34
|
+
const maxBuffer = opts.maxBuffer || DEFAULT_MAX_BUFFER;
|
|
35
|
+
const encoding = opts.encoding || 'utf8';
|
|
36
|
+
|
|
37
|
+
return new Promise((resolve) => {
|
|
38
|
+
const child = spawn('ssh', args, {
|
|
39
|
+
env: process.env,
|
|
40
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
41
|
+
});
|
|
42
|
+
let stdout = '';
|
|
43
|
+
let stderr = '';
|
|
44
|
+
let stdoutSize = 0;
|
|
45
|
+
let stderrSize = 0;
|
|
46
|
+
let settled = false;
|
|
47
|
+
|
|
48
|
+
const finish = (result) => {
|
|
49
|
+
if (settled) return;
|
|
50
|
+
settled = true;
|
|
51
|
+
resolve(result);
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
const append = (name, chunk) => {
|
|
55
|
+
const text = Buffer.isBuffer(chunk) ? chunk.toString(encoding) : String(chunk);
|
|
56
|
+
const size = Buffer.byteLength(text);
|
|
57
|
+
if (name === 'stdout') {
|
|
58
|
+
stdout += text;
|
|
59
|
+
stdoutSize += size;
|
|
60
|
+
} else {
|
|
61
|
+
stderr += text;
|
|
62
|
+
stderrSize += size;
|
|
63
|
+
}
|
|
64
|
+
if (stdoutSize + stderrSize > maxBuffer) {
|
|
65
|
+
child.kill();
|
|
66
|
+
const error = new Error('spawn ssh ENOBUFS');
|
|
67
|
+
error.code = 'ENOBUFS';
|
|
68
|
+
finish({ error, stdout, stderr, status: null, signal: 'SIGTERM' });
|
|
69
|
+
}
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
child.stdout.on('data', (chunk) => append('stdout', chunk));
|
|
73
|
+
child.stderr.on('data', (chunk) => append('stderr', chunk));
|
|
74
|
+
child.on('error', (error) => finish({ error, stdout, stderr, status: null, signal: null }));
|
|
75
|
+
child.on('close', (status, signal) => finish({ stdout, stderr, status, signal }));
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
|
|
28
79
|
function runRemoteCwbJson(server, argv) {
|
|
29
80
|
const result = runRemoteCwb(server, argv, { encoding: 'utf8' });
|
|
30
|
-
if (result.error)
|
|
81
|
+
if (result.error) {
|
|
82
|
+
if (result.error.code === 'ENOBUFS') {
|
|
83
|
+
throw new Error('remote output exceeded buffer; update the remote codex-workbench so compact listing is available');
|
|
84
|
+
}
|
|
85
|
+
throw result.error;
|
|
86
|
+
}
|
|
87
|
+
if (result.status !== 0) {
|
|
88
|
+
const stderr = (result.stderr || '').trim();
|
|
89
|
+
throw new Error(stderr || `ssh exited with code ${result.status}`);
|
|
90
|
+
}
|
|
91
|
+
return JSON.parse(result.stdout || 'null');
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
async function runRemoteCwbJsonAsync(server, argv) {
|
|
95
|
+
const result = await runRemoteCwbAsync(server, argv, { encoding: 'utf8' });
|
|
96
|
+
if (result.error) {
|
|
97
|
+
if (result.error.code === 'ENOBUFS') {
|
|
98
|
+
throw new Error('remote output exceeded buffer; update the remote codex-workbench so compact listing is available');
|
|
99
|
+
}
|
|
100
|
+
throw result.error;
|
|
101
|
+
}
|
|
31
102
|
if (result.status !== 0) {
|
|
32
103
|
const stderr = (result.stderr || '').trim();
|
|
33
104
|
throw new Error(stderr || `ssh exited with code ${result.status}`);
|
|
@@ -38,6 +109,8 @@ function runRemoteCwbJson(server, argv) {
|
|
|
38
109
|
module.exports = {
|
|
39
110
|
remoteCwbCommand,
|
|
40
111
|
runRemoteCwb,
|
|
112
|
+
runRemoteCwbAsync,
|
|
41
113
|
runRemoteCwbJson,
|
|
114
|
+
runRemoteCwbJsonAsync,
|
|
42
115
|
sshBaseArgs,
|
|
43
116
|
};
|
package/src/ui/workbench.js
CHANGED
|
@@ -9,6 +9,8 @@ const {
|
|
|
9
9
|
LOCAL_SOURCE,
|
|
10
10
|
createSourceDirectory,
|
|
11
11
|
listSourceDirectories,
|
|
12
|
+
loadLocalWorkbenchSessions,
|
|
13
|
+
loadRemoteSourceSessions,
|
|
12
14
|
loadWorkbenchSessions,
|
|
13
15
|
runSourceNewSession,
|
|
14
16
|
runSourceSessionCommand,
|
|
@@ -34,6 +36,9 @@ async function runWorkbench() {
|
|
|
34
36
|
let syncingProjects = false;
|
|
35
37
|
let projectWidth = 32;
|
|
36
38
|
let activePanel = 'projects';
|
|
39
|
+
let remoteLoadId = 0;
|
|
40
|
+
let remoteLoading = false;
|
|
41
|
+
let closed = false;
|
|
37
42
|
|
|
38
43
|
const screen = blessed.screen({
|
|
39
44
|
smartCSR: true,
|
|
@@ -159,6 +164,17 @@ async function runWorkbench() {
|
|
|
159
164
|
|
|
160
165
|
const currentGroup = () => groups[groupIndex] || groups[0] || { kind: 'all', source: null, cwd: null };
|
|
161
166
|
|
|
167
|
+
const groupKey = (group) => {
|
|
168
|
+
if (!group || group.kind === 'all') return 'all';
|
|
169
|
+
if (group.kind === 'source') return `source:${group.source.id}`;
|
|
170
|
+
return `project:${group.source.id}:${group.cwd}`;
|
|
171
|
+
};
|
|
172
|
+
|
|
173
|
+
const restoreGroupKey = (key) => {
|
|
174
|
+
const index = groups.findIndex((group) => groupKey(group) === key);
|
|
175
|
+
if (index !== -1) groupIndex = index;
|
|
176
|
+
};
|
|
177
|
+
|
|
162
178
|
const currentSessions = () => {
|
|
163
179
|
const group = currentGroup();
|
|
164
180
|
if (group.kind === 'all') return sessions;
|
|
@@ -220,20 +236,101 @@ async function runWorkbench() {
|
|
|
220
236
|
status.style.fg = isError ? 'red' : 'white';
|
|
221
237
|
};
|
|
222
238
|
|
|
223
|
-
const
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
239
|
+
const visibleSession = (session) => !session.archived && !session.hidden;
|
|
240
|
+
|
|
241
|
+
const sortSessionList = (list) => {
|
|
242
|
+
list.sort((a, b) => new Date(b.updatedAt) - new Date(a.updatedAt));
|
|
243
|
+
return list;
|
|
244
|
+
};
|
|
245
|
+
|
|
246
|
+
const setSourceErrorMessage = () => {
|
|
247
|
+
if (!sourceErrors.length) return false;
|
|
248
|
+
const first = sourceErrors[0];
|
|
249
|
+
const detail = `${first.source.label}: ${first.error}`;
|
|
250
|
+
const prefix = sourceErrors.length === 1 ? 'Remote source failed' : `${sourceErrors.length} remote sources failed`;
|
|
251
|
+
setMessage(`${prefix}: ${truncate(detail, 100)}`, true);
|
|
252
|
+
return true;
|
|
253
|
+
};
|
|
254
|
+
|
|
255
|
+
const updateSessionViews = (preferredGroupKey = groupKey(currentGroup())) => {
|
|
228
256
|
groups = buildGroups();
|
|
257
|
+
restoreGroupKey(preferredGroupKey);
|
|
229
258
|
if (groupIndex >= groups.length) groupIndex = Math.max(0, groups.length - 1);
|
|
230
259
|
const visible = currentSessions();
|
|
231
260
|
if (selected >= visible.length) selected = Math.max(0, visible.length - 1);
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
261
|
+
};
|
|
262
|
+
|
|
263
|
+
const reloadLocal = (preserveRemote = true) => {
|
|
264
|
+
const preferredGroupKey = groupKey(currentGroup());
|
|
265
|
+
const state = loadLocalWorkbenchSessions();
|
|
266
|
+
const sourceIds = new Set(state.sources.map((source) => source.id));
|
|
267
|
+
const remoteSessions = preserveRemote
|
|
268
|
+
? sessions.filter((session) => session.sourceRemote && sourceIds.has(session.sourceId))
|
|
269
|
+
: [];
|
|
270
|
+
sources = state.sources;
|
|
271
|
+
sourceErrors = sourceErrors.filter((item) => sourceIds.has(item.source.id));
|
|
272
|
+
sessions = sortSessionList([...state.sessions, ...remoteSessions].filter(visibleSession));
|
|
273
|
+
updateSessionViews(preferredGroupKey);
|
|
274
|
+
};
|
|
275
|
+
|
|
276
|
+
const replaceSourceSessions = (source, sourceSessions) => {
|
|
277
|
+
const preferredGroupKey = groupKey(currentGroup());
|
|
278
|
+
sessions = sortSessionList([
|
|
279
|
+
...sessions.filter((session) => session.sourceId !== source.id),
|
|
280
|
+
...sourceSessions.filter(visibleSession),
|
|
281
|
+
]);
|
|
282
|
+
updateSessionViews(preferredGroupKey);
|
|
283
|
+
};
|
|
284
|
+
|
|
285
|
+
const renderRemoteUpdate = () => {
|
|
286
|
+
if (closed) return;
|
|
287
|
+
syncProjects();
|
|
288
|
+
syncList();
|
|
289
|
+
render();
|
|
290
|
+
};
|
|
291
|
+
|
|
292
|
+
const startRemoteReload = (quiet = false) => {
|
|
293
|
+
const remoteSources = sources.filter((source) => source.remote);
|
|
294
|
+
remoteLoadId += 1;
|
|
295
|
+
const loadId = remoteLoadId;
|
|
296
|
+
sourceErrors = [];
|
|
297
|
+
if (!remoteSources.length) {
|
|
298
|
+
remoteLoading = false;
|
|
299
|
+
return;
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
remoteLoading = true;
|
|
303
|
+
let completed = 0;
|
|
304
|
+
if (!quiet && (!message || message === 'Ready')) {
|
|
305
|
+
setMessage(`Loading ${remoteSources.length} remote source${remoteSources.length === 1 ? '' : 's'}...`);
|
|
306
|
+
renderRemoteUpdate();
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
for (const source of remoteSources) {
|
|
310
|
+
loadRemoteSourceSessions(source)
|
|
311
|
+
.then((sourceSessions) => {
|
|
312
|
+
if (closed || loadId !== remoteLoadId) return;
|
|
313
|
+
replaceSourceSessions(source, sourceSessions);
|
|
314
|
+
})
|
|
315
|
+
.catch((err) => {
|
|
316
|
+
if (closed || loadId !== remoteLoadId) return;
|
|
317
|
+
sourceErrors.push({ source, error: err.message });
|
|
318
|
+
})
|
|
319
|
+
.finally(() => {
|
|
320
|
+
if (closed || loadId !== remoteLoadId) return;
|
|
321
|
+
completed += 1;
|
|
322
|
+
remoteLoading = completed < remoteSources.length;
|
|
323
|
+
if (remoteLoading) {
|
|
324
|
+
if (!quiet && message.startsWith('Loading ')) {
|
|
325
|
+
setMessage(`Loading remote sources... ${completed}/${remoteSources.length}`);
|
|
326
|
+
}
|
|
327
|
+
} else if (sourceErrors.length) {
|
|
328
|
+
setSourceErrorMessage();
|
|
329
|
+
} else if (message.startsWith('Loading ')) {
|
|
330
|
+
setMessage('Remote sources loaded.');
|
|
331
|
+
}
|
|
332
|
+
renderRemoteUpdate();
|
|
333
|
+
});
|
|
237
334
|
}
|
|
238
335
|
};
|
|
239
336
|
|
|
@@ -356,12 +453,13 @@ async function runWorkbench() {
|
|
|
356
453
|
const promptOpen = () => prompt.visible || question.visible || directoryPicker.isOpen();
|
|
357
454
|
|
|
358
455
|
const leaveScreen = () => {
|
|
456
|
+
closed = true;
|
|
359
457
|
screen.destroy();
|
|
360
458
|
};
|
|
361
459
|
|
|
362
460
|
const refreshAfterAction = (text, isError = false, focusCwd = null, focusSourceId = null) => {
|
|
363
461
|
setMessage(text, isError);
|
|
364
|
-
|
|
462
|
+
reloadLocal();
|
|
365
463
|
if (focusCwd) {
|
|
366
464
|
const nextGroupIndex = groups.findIndex((group) => {
|
|
367
465
|
return group.kind === 'project' &&
|
|
@@ -373,6 +471,7 @@ async function runWorkbench() {
|
|
|
373
471
|
syncProjects();
|
|
374
472
|
syncList();
|
|
375
473
|
render();
|
|
474
|
+
startRemoteReload(true);
|
|
376
475
|
};
|
|
377
476
|
|
|
378
477
|
const selectGroup = (index) => {
|
|
@@ -453,8 +552,9 @@ async function runWorkbench() {
|
|
|
453
552
|
}
|
|
454
553
|
};
|
|
455
554
|
|
|
456
|
-
|
|
457
|
-
|
|
555
|
+
reloadLocal(false);
|
|
556
|
+
const remoteSourceCount = sources.filter((source) => source.remote).length;
|
|
557
|
+
setMessage(remoteSourceCount ? `Loading ${remoteSourceCount} remote source${remoteSourceCount === 1 ? '' : 's'}...` : 'Ready');
|
|
458
558
|
applyLayout();
|
|
459
559
|
syncProjects();
|
|
460
560
|
syncList();
|
|
@@ -648,6 +748,7 @@ async function runWorkbench() {
|
|
|
648
748
|
|
|
649
749
|
projectsList.focus();
|
|
650
750
|
render();
|
|
751
|
+
startRemoteReload(true);
|
|
651
752
|
|
|
652
753
|
return new Promise(() => {});
|
|
653
754
|
}
|