@karpeleslab/teamclaude 1.0.3 → 1.0.4
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/package.json +1 -1
- package/src/account-manager.js +15 -34
- package/src/config.js +12 -0
- package/src/index.js +65 -10
- package/src/server.js +7 -10
- package/src/tui.js +104 -12
package/package.json
CHANGED
package/src/account-manager.js
CHANGED
|
@@ -23,6 +23,7 @@ export class AccountManager {
|
|
|
23
23
|
index,
|
|
24
24
|
name: acct.name,
|
|
25
25
|
type: acct.type,
|
|
26
|
+
accountUuid: acct.accountUuid || null,
|
|
26
27
|
credential: acct.accessToken || acct.apiKey,
|
|
27
28
|
refreshToken: acct.refreshToken || null,
|
|
28
29
|
expiresAt: acct.expiresAt || null,
|
|
@@ -268,42 +269,21 @@ export class AccountManager {
|
|
|
268
269
|
}
|
|
269
270
|
|
|
270
271
|
/**
|
|
271
|
-
*
|
|
272
|
-
* Updates the first OAuth account whose credential matches the old token,
|
|
273
|
-
* or the first expired/error OAuth account if none match.
|
|
274
|
-
*
|
|
275
|
-
* @param {string} accessToken - The new access token
|
|
276
|
-
* @param {string} [refreshToken] - New refresh token (if available from intercepted refresh)
|
|
277
|
-
* @param {number} [expiresAt] - Token expiry timestamp
|
|
272
|
+
* Update a specific account's OAuth tokens (e.g. after intercepting a token refresh).
|
|
278
273
|
*/
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
if (
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
}
|
|
290
|
-
|
|
291
|
-
// Find the best OAuth account to update: prefer expired/error accounts
|
|
292
|
-
const candidate = this.accounts.find(a =>
|
|
293
|
-
a.type === 'oauth' && (a.status === 'error' || isTokenExpiringSoon(a.expiresAt, 0))
|
|
294
|
-
) || this.accounts.find(a => a.type === 'oauth');
|
|
295
|
-
|
|
296
|
-
if (!candidate) return;
|
|
297
|
-
|
|
298
|
-
candidate.credential = accessToken;
|
|
299
|
-
if (refreshToken) candidate.refreshToken = refreshToken;
|
|
300
|
-
candidate.expiresAt = expiresAt || Date.now() + 3600 * 1000;
|
|
301
|
-
if (candidate.status === 'error') candidate.status = 'active';
|
|
302
|
-
console.log(`[TeamClaude] Captured fresh token for account "${candidate.name}"`);
|
|
303
|
-
this._onTokenRefresh?.(candidate.index, {
|
|
274
|
+
updateAccountTokens(accountIndex, { accessToken, refreshToken, expiresAt }) {
|
|
275
|
+
const account = this.accounts[accountIndex];
|
|
276
|
+
if (!account || account.type !== 'oauth') return;
|
|
277
|
+
|
|
278
|
+
account.credential = accessToken;
|
|
279
|
+
if (refreshToken) account.refreshToken = refreshToken;
|
|
280
|
+
account.expiresAt = expiresAt;
|
|
281
|
+
if (account.status === 'error') account.status = 'active';
|
|
282
|
+
console.log(`[TeamClaude] Updated tokens for account "${account.name}"`);
|
|
283
|
+
this._onTokenRefresh?.(accountIndex, {
|
|
304
284
|
accessToken,
|
|
305
|
-
refreshToken:
|
|
306
|
-
expiresAt:
|
|
285
|
+
refreshToken: account.refreshToken,
|
|
286
|
+
expiresAt: account.expiresAt,
|
|
307
287
|
});
|
|
308
288
|
}
|
|
309
289
|
|
|
@@ -316,6 +296,7 @@ export class AccountManager {
|
|
|
316
296
|
index,
|
|
317
297
|
name: acctData.name,
|
|
318
298
|
type: acctData.type,
|
|
299
|
+
accountUuid: acctData.accountUuid || null,
|
|
319
300
|
credential: acctData.accessToken || acctData.apiKey,
|
|
320
301
|
refreshToken: acctData.refreshToken || null,
|
|
321
302
|
expiresAt: acctData.expiresAt || null,
|
package/src/config.js
CHANGED
|
@@ -46,3 +46,15 @@ export async function saveConfig(config) {
|
|
|
46
46
|
await mkdir(dirname(path), { recursive: true });
|
|
47
47
|
await writeFile(path, JSON.stringify(config, null, 2) + '\n', { mode: 0o600 });
|
|
48
48
|
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Atomically update the config: re-reads from disk, calls updater(config),
|
|
52
|
+
* then saves. Returns the updated config. This prevents overwriting changes
|
|
53
|
+
* made by other processes (e.g. `teamclaude import` while the server runs).
|
|
54
|
+
*/
|
|
55
|
+
export async function atomicConfigUpdate(updater) {
|
|
56
|
+
const config = await loadConfig() || createDefaultConfig();
|
|
57
|
+
await updater(config);
|
|
58
|
+
await saveConfig(config);
|
|
59
|
+
return config;
|
|
60
|
+
}
|
package/src/index.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
import { spawn } from 'node:child_process';
|
|
4
4
|
import { createInterface } from 'node:readline';
|
|
5
|
-
import { loadOrCreateConfig, saveConfig, getConfigPath } from './config.js';
|
|
5
|
+
import { loadOrCreateConfig, loadConfig, saveConfig, atomicConfigUpdate, getConfigPath } from './config.js';
|
|
6
6
|
import { AccountManager } from './account-manager.js';
|
|
7
7
|
import { createProxyServer } from './server.js';
|
|
8
8
|
import { importCredentials, loginOAuth, fetchProfile } from './oauth.js';
|
|
@@ -82,15 +82,21 @@ async function serverCommand() {
|
|
|
82
82
|
const threshold = config.switchThreshold || 0.98;
|
|
83
83
|
const accountManager = new AccountManager(accounts, threshold);
|
|
84
84
|
|
|
85
|
-
// Persist refreshed tokens back to config
|
|
85
|
+
// Persist refreshed tokens back to config (re-read from disk to avoid clobbering
|
|
86
|
+
// accounts added externally, e.g. by `teamclaude import` while server is running)
|
|
86
87
|
accountManager.onTokenRefresh((idx, newTokens) => {
|
|
87
|
-
const
|
|
88
|
-
if (
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
88
|
+
const account = accountManager.accounts[idx];
|
|
89
|
+
if (!account) return;
|
|
90
|
+
atomicConfigUpdate(diskConfig => {
|
|
91
|
+
syncNewAccountsFromDisk(diskConfig, config, accountManager);
|
|
92
|
+
// Match by UUID first, then by name — index may have shifted
|
|
93
|
+
const cfgIdx = findConfigAccount(diskConfig, account);
|
|
94
|
+
if (cfgIdx >= 0) {
|
|
95
|
+
diskConfig.accounts[cfgIdx].accessToken = newTokens.accessToken;
|
|
96
|
+
diskConfig.accounts[cfgIdx].refreshToken = newTokens.refreshToken;
|
|
97
|
+
diskConfig.accounts[cfgIdx].expiresAt = newTokens.expiresAt;
|
|
98
|
+
}
|
|
99
|
+
}).catch(err => console.error(`[TeamClaude] Failed to save refreshed token: ${err.message}`));
|
|
94
100
|
});
|
|
95
101
|
const port = config.proxy.port;
|
|
96
102
|
const useTUI = process.stdout.isTTY && process.stdin.isTTY;
|
|
@@ -100,7 +106,24 @@ async function serverCommand() {
|
|
|
100
106
|
|
|
101
107
|
if (useTUI) {
|
|
102
108
|
tui = new TUI({
|
|
103
|
-
accountManager, config,
|
|
109
|
+
accountManager, config,
|
|
110
|
+
saveConfig: () => atomicConfigUpdate(diskConfig => {
|
|
111
|
+
syncNewAccountsFromDisk(diskConfig, config, accountManager);
|
|
112
|
+
// Write in-memory accounts back, preserving extra disk-only fields
|
|
113
|
+
diskConfig.accounts = config.accounts.map(a => {
|
|
114
|
+
const diskAcct = diskConfig.accounts.find(
|
|
115
|
+
d => (a.accountUuid && d.accountUuid === a.accountUuid) || d.name === a.name
|
|
116
|
+
);
|
|
117
|
+
return diskAcct ? { ...diskAcct, ...a } : a;
|
|
118
|
+
});
|
|
119
|
+
}),
|
|
120
|
+
syncAccounts: async () => {
|
|
121
|
+
const diskConfig = await loadConfig();
|
|
122
|
+
if (!diskConfig) return 0;
|
|
123
|
+
const before = accountManager.accounts.length;
|
|
124
|
+
syncNewAccountsFromDisk(diskConfig, config, accountManager);
|
|
125
|
+
return accountManager.accounts.length - before;
|
|
126
|
+
},
|
|
104
127
|
onQuit: () => { server.close(() => process.exit(0)); },
|
|
105
128
|
});
|
|
106
129
|
hooks = {
|
|
@@ -559,6 +582,38 @@ async function upsertOAuthAccount(config, name, creds, source = 'unknown') {
|
|
|
559
582
|
console.log(`Saved to ${getConfigPath()}`);
|
|
560
583
|
}
|
|
561
584
|
|
|
585
|
+
// ── config sync helpers ─────────────────────────────────────
|
|
586
|
+
|
|
587
|
+
/**
|
|
588
|
+
* Find a config account entry matching an in-memory account (by UUID, then name).
|
|
589
|
+
*/
|
|
590
|
+
function findConfigAccount(diskConfig, account) {
|
|
591
|
+
if (account.accountUuid) {
|
|
592
|
+
const idx = diskConfig.accounts.findIndex(a => a.accountUuid === account.accountUuid);
|
|
593
|
+
if (idx >= 0) return idx;
|
|
594
|
+
}
|
|
595
|
+
return diskConfig.accounts.findIndex(a => a.name === account.name);
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
/**
|
|
599
|
+
* Detect accounts added to disk config by external processes and add them
|
|
600
|
+
* to the running AccountManager + in-memory config.
|
|
601
|
+
*/
|
|
602
|
+
function syncNewAccountsFromDisk(diskConfig, memConfig, accountManager) {
|
|
603
|
+
for (const diskAcct of diskConfig.accounts) {
|
|
604
|
+
const knownByUuid = diskAcct.accountUuid &&
|
|
605
|
+
memConfig.accounts.some(a => a.accountUuid === diskAcct.accountUuid);
|
|
606
|
+
const knownByName = memConfig.accounts.some(a => a.name === diskAcct.name);
|
|
607
|
+
|
|
608
|
+
if (!knownByUuid && !knownByName) {
|
|
609
|
+
// New account discovered on disk — add to running server
|
|
610
|
+
memConfig.accounts.push(diskAcct);
|
|
611
|
+
accountManager.addAccount(diskAcct);
|
|
612
|
+
console.log(`[TeamClaude] Picked up new account "${diskAcct.name}" from config`);
|
|
613
|
+
}
|
|
614
|
+
}
|
|
615
|
+
}
|
|
616
|
+
|
|
562
617
|
// ── helpers ─────────────────────────────────────────────────
|
|
563
618
|
|
|
564
619
|
async function resolveAccounts(config) {
|
package/src/server.js
CHANGED
|
@@ -118,13 +118,16 @@ async function handleTokenRefresh(req, res, accountManager, hooks) {
|
|
|
118
118
|
|
|
119
119
|
const responseBody = await upstreamRes.text();
|
|
120
120
|
|
|
121
|
-
// Capture tokens from successful refresh
|
|
121
|
+
// Capture tokens from successful refresh — update the current account directly
|
|
122
122
|
if (upstreamRes.ok) {
|
|
123
123
|
try {
|
|
124
124
|
const tokens = JSON.parse(responseBody);
|
|
125
125
|
if (tokens.access_token) {
|
|
126
|
-
accountManager.
|
|
127
|
-
|
|
126
|
+
accountManager.updateAccountTokens(accountManager.currentIndex, {
|
|
127
|
+
accessToken: tokens.access_token,
|
|
128
|
+
refreshToken: tokens.refresh_token,
|
|
129
|
+
expiresAt: tokens.expires_at || (Date.now() + (tokens.expires_in || 3600) * 1000),
|
|
130
|
+
});
|
|
128
131
|
}
|
|
129
132
|
} catch {}
|
|
130
133
|
}
|
|
@@ -226,12 +229,6 @@ async function forwardRequest(req, res, body, accountManager, upstream, retryCou
|
|
|
226
229
|
headers['x-api-key'] = account.credential;
|
|
227
230
|
}
|
|
228
231
|
|
|
229
|
-
// Capture fresh Bearer tokens from the client to keep stored credentials up to date
|
|
230
|
-
const clientBearer = req.headers['authorization']?.match(/^Bearer (.+)/i)?.[1];
|
|
231
|
-
if (clientBearer) {
|
|
232
|
-
accountManager.captureClientToken(clientBearer);
|
|
233
|
-
}
|
|
234
|
-
|
|
235
232
|
const upstreamUrl = `${upstream}${req.url}`;
|
|
236
233
|
const method = req.method;
|
|
237
234
|
|
|
@@ -332,7 +329,7 @@ async function forwardRequest(req, res, body, accountManager, upstream, retryCou
|
|
|
332
329
|
writeRequestLog(logDir, reqId, logSections);
|
|
333
330
|
}
|
|
334
331
|
|
|
335
|
-
if (retryCount < maxRetries) {
|
|
332
|
+
if (retryCount < maxRetries && !res.headersSent) {
|
|
336
333
|
account.status = 'error';
|
|
337
334
|
return forwardRequest(req, res, body, accountManager, upstream, retryCount + 1, hooks, reqId, ctx, logDir);
|
|
338
335
|
}
|
package/src/tui.js
CHANGED
|
@@ -17,7 +17,8 @@ const red = s => fg(31, s);
|
|
|
17
17
|
const cyan = s => fg(36, s);
|
|
18
18
|
const gray = s => fg(90, s);
|
|
19
19
|
|
|
20
|
-
const
|
|
20
|
+
const ANSI_RE = /\x1b\[[0-9;]*m/g;
|
|
21
|
+
const strip = s => s.replace(ANSI_RE, '');
|
|
21
22
|
const vw = s => strip(s).length;
|
|
22
23
|
|
|
23
24
|
function rpad(s, w) {
|
|
@@ -25,13 +26,85 @@ function rpad(s, w) {
|
|
|
25
26
|
return gap > 0 ? s + ' '.repeat(gap) : s;
|
|
26
27
|
}
|
|
27
28
|
|
|
28
|
-
|
|
29
|
-
|
|
29
|
+
/** Truncate a string with ANSI codes to exactly w visible characters, then reset. */
|
|
30
|
+
function truncate(s, w) {
|
|
31
|
+
let visible = 0;
|
|
32
|
+
let out = '';
|
|
33
|
+
let i = 0;
|
|
34
|
+
while (i < s.length && visible < w) {
|
|
35
|
+
if (s[i] === '\x1b') {
|
|
36
|
+
const end = s.indexOf('m', i);
|
|
37
|
+
if (end >= 0) { out += s.slice(i, end + 1); i = end + 1; continue; }
|
|
38
|
+
}
|
|
39
|
+
out += s[i];
|
|
40
|
+
visible++;
|
|
41
|
+
i++;
|
|
42
|
+
}
|
|
43
|
+
return out + RESET;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** Fit a line to exactly w columns: truncate if too long, pad if too short. */
|
|
47
|
+
function fitLine(s, w) {
|
|
48
|
+
const v = vw(s);
|
|
49
|
+
if (v > w) return truncate(s, w);
|
|
50
|
+
if (v < w) return s + ' '.repeat(w - v);
|
|
51
|
+
return s;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function formatReset(resetTs) {
|
|
55
|
+
if (!resetTs) return '';
|
|
56
|
+
const ms = resetTs - Date.now();
|
|
57
|
+
if (ms <= 0) return '';
|
|
58
|
+
const mins = Math.ceil(ms / 60000);
|
|
59
|
+
if (mins < 60) return `${mins}m`;
|
|
60
|
+
const hrs = Math.floor(mins / 60);
|
|
61
|
+
const rm = mins % 60;
|
|
62
|
+
if (hrs < 24) return rm > 0 ? `${hrs}h${rm}m` : `${hrs}h`;
|
|
63
|
+
const days = Math.floor(hrs / 24);
|
|
64
|
+
const rh = hrs % 24;
|
|
65
|
+
return rh > 0 ? `${days}d${rh}h` : `${days}d`;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Render a progress bar using background colors with text overlaid.
|
|
70
|
+
* The label (e.g. "Ses 2h30m" or "45%") is drawn on top of the bar.
|
|
71
|
+
*/
|
|
72
|
+
function bar(ratio, w = 10, resetTs) {
|
|
73
|
+
const rst = formatReset(resetTs);
|
|
74
|
+
|
|
75
|
+
if (ratio == null || isNaN(ratio)) {
|
|
76
|
+
// No data — dim background, show label or dash
|
|
77
|
+
const label = rst || '-';
|
|
78
|
+
const text = label.slice(0, w);
|
|
79
|
+
const pad = w - text.length;
|
|
80
|
+
const lp = Math.floor(pad / 2);
|
|
81
|
+
const rp = pad - lp;
|
|
82
|
+
return `${ESC}100m${' '.repeat(lp)}${text}${' '.repeat(rp)}${RESET}`;
|
|
83
|
+
}
|
|
84
|
+
|
|
30
85
|
ratio = Math.max(0, Math.min(1, ratio));
|
|
31
86
|
const f = Math.round(ratio * w);
|
|
32
|
-
|
|
33
|
-
const
|
|
34
|
-
|
|
87
|
+
// Background colors: 42=green, 43=yellow, 41=red; 100=bright black (gray) for empty
|
|
88
|
+
const bg = ratio < 0.7 ? 42 : ratio < 0.9 ? 43 : 41;
|
|
89
|
+
|
|
90
|
+
// Build the label to overlay: show reset time if available, else percentage
|
|
91
|
+
const pct = (ratio * 100).toFixed(0) + '%';
|
|
92
|
+
const label = rst || pct;
|
|
93
|
+
const text = label.slice(0, w);
|
|
94
|
+
const pad = w - text.length;
|
|
95
|
+
const lp = Math.floor(pad / 2);
|
|
96
|
+
const rp = pad - lp;
|
|
97
|
+
const chars = (' '.repeat(lp) + text + ' '.repeat(rp));
|
|
98
|
+
|
|
99
|
+
// Split chars into filled (colored bg) and empty (gray bg) portions
|
|
100
|
+
const filled = chars.slice(0, f);
|
|
101
|
+
const empty = chars.slice(f);
|
|
102
|
+
|
|
103
|
+
let out = '';
|
|
104
|
+
if (filled) out += `${ESC}${bg};97m${filled}`;
|
|
105
|
+
if (empty) out += `${ESC}100;37m${empty}`;
|
|
106
|
+
out += RESET;
|
|
107
|
+
return out;
|
|
35
108
|
}
|
|
36
109
|
|
|
37
110
|
function timestamp() {
|
|
@@ -41,10 +114,11 @@ function timestamp() {
|
|
|
41
114
|
// ── TUI class ────────────────────────────────────────────────
|
|
42
115
|
|
|
43
116
|
export class TUI {
|
|
44
|
-
constructor({ accountManager, config, saveConfig, onQuit }) {
|
|
117
|
+
constructor({ accountManager, config, saveConfig, syncAccounts, onQuit }) {
|
|
45
118
|
this.am = accountManager;
|
|
46
119
|
this.config = config;
|
|
47
120
|
this.saveConfig = saveConfig;
|
|
121
|
+
this.syncAccounts = syncAccounts;
|
|
48
122
|
this.onQuit = onQuit;
|
|
49
123
|
|
|
50
124
|
this.log = []; // completed activity entries
|
|
@@ -159,6 +233,7 @@ export class TUI {
|
|
|
159
233
|
this.mode = 'select'; this.selAction = 'remove'; this.selIdx = 0;
|
|
160
234
|
}
|
|
161
235
|
else if (k === 'a') { this.mode = 'add'; }
|
|
236
|
+
else if (k === 'R') { this._doSync(); }
|
|
162
237
|
}
|
|
163
238
|
|
|
164
239
|
_keySelect(k) {
|
|
@@ -202,6 +277,19 @@ export class TUI {
|
|
|
202
277
|
|
|
203
278
|
// ── account operations ─────────────────────────────
|
|
204
279
|
|
|
280
|
+
async _doSync() {
|
|
281
|
+
try {
|
|
282
|
+
const count = await this.syncAccounts();
|
|
283
|
+
if (count > 0) {
|
|
284
|
+
this._addLog(`Synced ${count} new account(s) from config`);
|
|
285
|
+
} else {
|
|
286
|
+
this._addLog('Config reloaded, no new accounts');
|
|
287
|
+
}
|
|
288
|
+
} catch (e) {
|
|
289
|
+
this._addLog(`Sync failed: ${e.message}`);
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
|
|
205
293
|
async _doImport() {
|
|
206
294
|
try {
|
|
207
295
|
const creds = await importCredentials('~/.claude/.credentials.json');
|
|
@@ -311,7 +399,7 @@ export class TUI {
|
|
|
311
399
|
// Write buffer
|
|
312
400
|
let buf = `${ESC}H`;
|
|
313
401
|
for (let i = 0; i < H; i++) {
|
|
314
|
-
buf +=
|
|
402
|
+
buf += fitLine(lines[i] || '', W);
|
|
315
403
|
if (i < H - 1) buf += '\r\n';
|
|
316
404
|
}
|
|
317
405
|
// Show cursor only in input mode
|
|
@@ -348,11 +436,13 @@ export class TUI {
|
|
|
348
436
|
|
|
349
437
|
// Quota ratios — prefer unified (Claude Max), fall back to standard (API key)
|
|
350
438
|
const q = a.quota;
|
|
351
|
-
let r1 = null, r2 = null, l1 = 'Ses', l2 = 'Wk ';
|
|
439
|
+
let r1 = null, r2 = null, l1 = 'Ses', l2 = 'Wk ', t1 = null, t2 = null;
|
|
352
440
|
|
|
353
441
|
if (q.unified5h != null || q.unified7d != null) {
|
|
354
442
|
r1 = q.unified5h;
|
|
355
443
|
r2 = q.unified7d;
|
|
444
|
+
t1 = q.unified5hReset;
|
|
445
|
+
t2 = q.unified7dReset;
|
|
356
446
|
} else {
|
|
357
447
|
l1 = 'Tok';
|
|
358
448
|
l2 = 'Req';
|
|
@@ -360,11 +450,13 @@ export class TUI {
|
|
|
360
450
|
? 1 - q.tokensRemaining / q.tokensLimit : null;
|
|
361
451
|
r2 = (q.requestsLimit != null && q.requestsRemaining != null)
|
|
362
452
|
? 1 - q.requestsRemaining / q.requestsLimit : null;
|
|
453
|
+
t1 = q.resetsAt ? new Date(q.resetsAt).getTime() : null;
|
|
454
|
+
t2 = t1;
|
|
363
455
|
}
|
|
364
456
|
|
|
365
|
-
let line = ` ${sel}${cur} ${name} ${type} ${status} ${l1} ${bar(r1, bw)}`;
|
|
457
|
+
let line = ` ${sel}${cur} ${name} ${type} ${status} ${l1} ${bar(r1, bw, t1)}`;
|
|
366
458
|
if (showBoth) {
|
|
367
|
-
line += ` ${l2} ${bar(r2, bw)}`;
|
|
459
|
+
line += ` ${l2} ${bar(r2, bw, t2)}`;
|
|
368
460
|
}
|
|
369
461
|
return line;
|
|
370
462
|
}
|
|
@@ -372,7 +464,7 @@ export class TUI {
|
|
|
372
464
|
_renderFooter() {
|
|
373
465
|
switch (this.mode) {
|
|
374
466
|
case 'normal':
|
|
375
|
-
return ` ${bold('s')}witch ${bold('a')}dd ${bold('r')}emove ${bold('q')}uit`;
|
|
467
|
+
return ` ${bold('s')}witch ${bold('a')}dd ${bold('r')}emove ${bold('R')}eload ${bold('q')}uit`;
|
|
376
468
|
case 'select': {
|
|
377
469
|
const act = this.selAction === 'switch' ? 'switch' : 'remove';
|
|
378
470
|
return ` ${dim('↑↓')} select ${bold('Enter')} ${act} ${bold('Esc')} cancel`;
|