@oddessentials/agent-guild 0.16.0 → 0.17.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/README.md +7 -0
- package/package.json +1 -1
- package/src/manager/providers.mjs +37 -9
- package/src/manager/session-manager.mjs +10 -4
- package/src/manager/shells.mjs +82 -0
- package/web/app.js +52 -2
- package/web/index.html +1 -0
- package/web/styles.css +1 -1
package/README.md
CHANGED
|
@@ -58,6 +58,13 @@ terminal: type instructions, answer prompts, watch output. Run as many as you
|
|
|
58
58
|
like. A local session manager owns them, so you can close or reload the page
|
|
59
59
|
and come back to the same screens.
|
|
60
60
|
|
|
61
|
+
**Pick your shell.** The Shell card offers the shells installed on your
|
|
62
|
+
computer and remembers your choice in this browser. On Windows it defaults
|
|
63
|
+
to PowerShell 7 when installed, then Windows PowerShell; Git Bash and Command
|
|
64
|
+
Prompt are also offered when found. On macOS and Linux it follows your login
|
|
65
|
+
shell. Pick the default chip to clear a saved choice and follow the default
|
|
66
|
+
again. The choice applies to new sessions.
|
|
67
|
+
|
|
61
68
|
<img src="docs/images/terminal.webp" alt="An open Claude Code session with its helper agents shown in the header">
|
|
62
69
|
|
|
63
70
|
**Agents and models at work.** Helper agents that a tool starts appear on its
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@oddessentials/agent-guild",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.17.0",
|
|
4
4
|
"description": "Launch and watch AI coding-assistant terminal sessions from one local web page. A bundled session manager owns the terminals so the page can close and reconnect.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -13,6 +13,7 @@ import { resolveCommand, resolveAllCommands, pathKey, buildSpawnSpec, runSpec }
|
|
|
13
13
|
import { compareVersions, probeVersion, fetchManifest, latestVersion, DEFAULT_NPM_REGISTRY } from './versions.mjs';
|
|
14
14
|
import { CHANNEL_LABELS, classifyInstall, expandHome, formatCommand, homeRelative, knownLaunchers, listInstallations, platformDependency, updateHelpAccepted } from './install-channels.mjs';
|
|
15
15
|
import { weavePaths } from './shell-env.mjs';
|
|
16
|
+
import { detectShells, fallbackShell } from './shells.mjs';
|
|
16
17
|
import { RUNNER, encodePlan } from './uninstall.mjs';
|
|
17
18
|
import { paths } from './config.mjs';
|
|
18
19
|
|
|
@@ -34,11 +35,6 @@ function refusal(status, code, message) {
|
|
|
34
35
|
return Object.assign(new Error(message), { status, code });
|
|
35
36
|
}
|
|
36
37
|
|
|
37
|
-
export function defaultShell(env = process.env, platform = process.platform) {
|
|
38
|
-
if (platform === 'win32') return 'powershell.exe';
|
|
39
|
-
return env.SHELL || (platform === 'darwin' ? '/bin/zsh' : '/bin/bash');
|
|
40
|
-
}
|
|
41
|
-
|
|
42
38
|
function readJson(file) {
|
|
43
39
|
return JSON.parse(fs.readFileSync(file, 'utf8'));
|
|
44
40
|
}
|
|
@@ -259,6 +255,7 @@ export class ProviderRegistry extends EventEmitter {
|
|
|
259
255
|
this._pathReadAt = 0;
|
|
260
256
|
this._pathPending = null;
|
|
261
257
|
this._installs = new Map();
|
|
258
|
+
this._shells = new Map();
|
|
262
259
|
this.versions = new Map();
|
|
263
260
|
this._refreshing = null;
|
|
264
261
|
this._npmRegistry = null;
|
|
@@ -272,6 +269,7 @@ export class ProviderRegistry extends EventEmitter {
|
|
|
272
269
|
this.warnings = warnings;
|
|
273
270
|
this._npmRegistry = null;
|
|
274
271
|
this._installs.clear();
|
|
272
|
+
this._shells.clear();
|
|
275
273
|
for (const w of warnings) console.warn(`[providers] ${w}`);
|
|
276
274
|
this.emit('updated');
|
|
277
275
|
}
|
|
@@ -335,6 +333,7 @@ export class ProviderRegistry extends EventEmitter {
|
|
|
335
333
|
if (next === (this.env[key] || '')) return false;
|
|
336
334
|
this.env[key] = next;
|
|
337
335
|
this._installs.clear();
|
|
336
|
+
this._shells.clear();
|
|
338
337
|
return true;
|
|
339
338
|
}
|
|
340
339
|
|
|
@@ -568,8 +567,32 @@ export class ProviderRegistry extends EventEmitter {
|
|
|
568
567
|
return this.accountFor(provider, found);
|
|
569
568
|
}
|
|
570
569
|
|
|
570
|
+
/** The shells the provider can start, or null when it is not the `@shell` provider. */
|
|
571
|
+
shellsFor(provider) {
|
|
572
|
+
if (provider.command !== '@shell') return null;
|
|
573
|
+
const cached = this._shells.get(provider.id);
|
|
574
|
+
if (cached && Date.now() - cached.at < INSTALLS_TTL_MS) return cached.found;
|
|
575
|
+
const found = detectShells({ ...this.env, ...provider.env }, this.platform);
|
|
576
|
+
this._shells.set(provider.id, { found, at: Date.now() });
|
|
577
|
+
return found;
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
/** The shell a session of the provider runs: the one named by `id`, else the default. Null for other providers. */
|
|
581
|
+
shellFor(provider, id = null) {
|
|
582
|
+
const found = this.shellsFor(provider);
|
|
583
|
+
if (!found) {
|
|
584
|
+
if (id == null) return null;
|
|
585
|
+
throw refusal(400, 'bad_shell', `${provider.tool} does not start a shell`);
|
|
586
|
+
}
|
|
587
|
+
const wanted = id ?? found.defaultId;
|
|
588
|
+
const shell = found.shells.find((s) => s.id === wanted) ?? null;
|
|
589
|
+
if (!shell && wanted !== null) throw refusal(409, 'shell_unavailable', `The shell "${wanted}" was not found on this computer`);
|
|
590
|
+
return shell;
|
|
591
|
+
}
|
|
592
|
+
|
|
571
593
|
commandFor(provider) {
|
|
572
|
-
|
|
594
|
+
if (provider.command !== '@shell') return provider.command;
|
|
595
|
+
return this.shellFor(provider)?.path ?? fallbackShell(this.env, this.platform);
|
|
573
596
|
}
|
|
574
597
|
|
|
575
598
|
resolve(provider) {
|
|
@@ -597,6 +620,7 @@ export class ProviderRegistry extends EventEmitter {
|
|
|
597
620
|
const channel = this.channelFor(provider, resolvedPath);
|
|
598
621
|
const update = this.updateFor(provider, channel);
|
|
599
622
|
const shown = (file) => homeRelative(file, { ...this.env, ...provider.env }, this.platform);
|
|
623
|
+
const shells = this.shellsFor(provider);
|
|
600
624
|
const installs = this.installsFor(provider, resolvedPath).map((install) => {
|
|
601
625
|
const active = install.resolvedPath === resolvedPath;
|
|
602
626
|
const copy = active ? { version: installed, status: versions?.versionStatus ?? null } : versions?.copies?.[install.resolvedPath];
|
|
@@ -645,6 +669,8 @@ export class ProviderRegistry extends EventEmitter {
|
|
|
645
669
|
reporting: provider.reporting,
|
|
646
670
|
reportingEnabled: this.reportingEnabled?.(provider) ?? null,
|
|
647
671
|
accounts: provider.accounts.map((account) => ({ id: account.id, label: account.label })),
|
|
672
|
+
shells: shells?.shells.map((shell) => ({ id: shell.id, label: shell.label, path: shell.path })) ?? null,
|
|
673
|
+
defaultShell: shells?.defaultId ?? null,
|
|
648
674
|
modelPattern: provider.modelPattern,
|
|
649
675
|
color: provider.color,
|
|
650
676
|
monogram: provider.monogram,
|
|
@@ -664,8 +690,8 @@ export class ProviderRegistry extends EventEmitter {
|
|
|
664
690
|
}
|
|
665
691
|
|
|
666
692
|
/** Spawn spec for node-pty, or throws with a user-facing message. */
|
|
667
|
-
spawnSpec(provider, extraArgs = [], resume = null, leadArgs = []) {
|
|
668
|
-
const resolved = this.resolve(provider);
|
|
693
|
+
spawnSpec(provider, extraArgs = [], resume = null, leadArgs = [], shell = null) {
|
|
694
|
+
const resolved = shell ? shell.path : this.resolve(provider);
|
|
669
695
|
if (!resolved) {
|
|
670
696
|
const hint = provider.install ? ` ${provider.install}` : '';
|
|
671
697
|
const err = new Error(`${provider.tool} ("${this.commandFor(provider)}") was not found on PATH.${hint}`);
|
|
@@ -683,7 +709,9 @@ export class ProviderRegistry extends EventEmitter {
|
|
|
683
709
|
}
|
|
684
710
|
resumeArgs = provider.resumeArgs.map((arg) => arg.replaceAll('{id}', resume));
|
|
685
711
|
}
|
|
686
|
-
|
|
712
|
+
// The provider's own args were written for its default shell, not for one picked instead.
|
|
713
|
+
const args = shell && shell.id !== this.shellsFor(provider).defaultId ? [] : provider.args;
|
|
714
|
+
return buildSpawnSpec(resolved, [...(shell?.args ?? []), ...leadArgs, ...args, ...resumeArgs, ...extraArgs], this.env, this.platform);
|
|
687
715
|
}
|
|
688
716
|
|
|
689
717
|
async updateSpec(provider) {
|
|
@@ -93,25 +93,31 @@ export class SessionManager extends EventEmitter {
|
|
|
93
93
|
return dir;
|
|
94
94
|
}
|
|
95
95
|
|
|
96
|
-
async create({ providerId, cwd, cols, rows, name, args, resume, account } = {}) {
|
|
96
|
+
async create({ providerId, cwd, cols, rows, name, args, resume, account, shell } = {}) {
|
|
97
97
|
const provider = this.registry.get(String(providerId || ''));
|
|
98
98
|
if (!provider) throw httpError(404, `unknown provider "${providerId}"`, 'unknown_provider');
|
|
99
99
|
if (args !== undefined && (!Array.isArray(args) || args.some((a) => typeof a !== 'string'))) {
|
|
100
100
|
throw httpError(400, 'args must be an array of strings', 'bad_args');
|
|
101
101
|
}
|
|
102
102
|
if (account !== undefined && account !== null && typeof account !== 'string') throw httpError(400, 'account must be a string', 'bad_account');
|
|
103
|
+
if (shell !== undefined && shell !== null && typeof shell !== 'string') throw httpError(400, 'shell must be a string', 'bad_shell');
|
|
103
104
|
const resumeId = cleanResumeId(resume);
|
|
104
105
|
const workDir = this.resolveCwd(cwd);
|
|
105
106
|
const signIn = this.registry.account(provider, account);
|
|
107
|
+
const runShell = this.registry.shellFor(provider, shell);
|
|
106
108
|
const hooks = this.sessionHooks ? await this.sessionHooks.launch(provider) : { args: [], reporting: null };
|
|
107
109
|
// Checked after the await, so an install that started meanwhile is seen.
|
|
108
110
|
if (this.installing.has(provider.id) || this.installsRunningFor(provider.id) > 0) {
|
|
109
111
|
throw httpError(409, `${provider.tool} is being installed, updated or removed; start it once that finishes`, 'install_in_progress');
|
|
110
112
|
}
|
|
111
|
-
const spawnSpec = this.registry.spawnSpec(provider, args || [], resumeId, hooks.args);
|
|
113
|
+
const spawnSpec = this.registry.spawnSpec(provider, args || [], resumeId, hooks.args, runShell);
|
|
112
114
|
this.prepareAccount(provider, signIn, { hooksSupplied: hooks.args.length > 0 });
|
|
113
|
-
const sessionName = cleanName(name)
|
|
114
|
-
|
|
115
|
+
const sessionName = cleanName(name)
|
|
116
|
+
|| (provider.accounts.length > 1 ? `${provider.tool} · ${signIn.label}` : null)
|
|
117
|
+
|| (runShell && this.registry.shellsFor(provider).shells.length > 1 ? `${provider.tool} · ${runShell.label}` : null);
|
|
118
|
+
const session = this._spawn({
|
|
119
|
+
provider, spawnSpec, cwd: workDir, cols, rows, name: sessionName, resume: resumeId, account: signIn, reporting: hooks.reporting, extraEnv: runShell?.env,
|
|
120
|
+
});
|
|
115
121
|
const model = modelFromArgs([...provider.args, ...(args || [])]);
|
|
116
122
|
if (model) session.setModel({ name: model }, 'args');
|
|
117
123
|
return session;
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
// The shells the Shell card can start: the ones installed on this computer,
|
|
2
|
+
// and which of them is the default.
|
|
3
|
+
|
|
4
|
+
import fs from 'node:fs';
|
|
5
|
+
import path from 'node:path';
|
|
6
|
+
import { resolveCommand } from './command-resolver.mjs';
|
|
7
|
+
|
|
8
|
+
const UNIX_SHELLS = [
|
|
9
|
+
{ id: 'bash', label: 'bash' },
|
|
10
|
+
{ id: 'zsh', label: 'zsh' },
|
|
11
|
+
{ id: 'fish', label: 'fish' },
|
|
12
|
+
{ id: 'pwsh', label: 'PowerShell' },
|
|
13
|
+
];
|
|
14
|
+
|
|
15
|
+
function isFile(file) {
|
|
16
|
+
try { return fs.statSync(file).isFile(); } catch { return false; }
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function envValue(env, name) {
|
|
20
|
+
const key = Object.keys(env).find((k) => k.toUpperCase() === name.toUpperCase());
|
|
21
|
+
return key ? env[key] : undefined;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// Git for Windows keeps bash.exe in <root>\bin, and git.exe in <root>\cmd,
|
|
25
|
+
// <root>\bin or <root>\mingw64\bin.
|
|
26
|
+
function findGitBash(env, resolve, exists) {
|
|
27
|
+
const p = path.win32;
|
|
28
|
+
const roots = [];
|
|
29
|
+
const git = resolve('git');
|
|
30
|
+
if (git) for (let dir = p.dirname(git), i = 0; i < 3; i++, dir = p.dirname(dir)) roots.push(dir);
|
|
31
|
+
for (const [name, sub] of [['ProgramFiles', 'Git'], ['ProgramW6432', 'Git'], ['ProgramFiles(x86)', 'Git'], ['LOCALAPPDATA', 'Programs\\Git'], ['USERPROFILE', 'scoop\\apps\\git\\current']]) {
|
|
32
|
+
const base = envValue(env, name);
|
|
33
|
+
if (base) roots.push(p.join(base, sub));
|
|
34
|
+
}
|
|
35
|
+
for (const root of roots) {
|
|
36
|
+
const bash = p.join(root, 'bin', 'bash.exe');
|
|
37
|
+
if (exists(bash) && exists(p.join(root, 'cmd', 'git.exe'))) return bash;
|
|
38
|
+
}
|
|
39
|
+
return null;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** The shell to name when none is installed. */
|
|
43
|
+
export function fallbackShell(env, platform) {
|
|
44
|
+
if (platform === 'win32') return 'powershell.exe';
|
|
45
|
+
return env.SHELL || (platform === 'darwin' ? '/bin/zsh' : '/bin/bash');
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function windowsShells(env, resolve, exists) {
|
|
49
|
+
const installed = ['ProgramFiles', 'ProgramW6432'].map((name) => envValue(env, name)).filter(Boolean).map((base) => path.win32.join(base, 'PowerShell', '7', 'pwsh.exe'));
|
|
50
|
+
const pwsh = resolve('pwsh') || installed.find(exists) || null;
|
|
51
|
+
const gitBash = findGitBash(env, resolve, exists);
|
|
52
|
+
const shells = [
|
|
53
|
+
pwsh && { id: 'pwsh', label: 'PowerShell', path: pwsh, args: [], env: {} },
|
|
54
|
+
{ id: 'powershell', label: 'Windows PowerShell', path: resolve('powershell.exe'), args: [], env: {} },
|
|
55
|
+
{ id: 'cmd', label: 'Command Prompt', path: resolve('cmd.exe'), args: [], env: {} },
|
|
56
|
+
// CHERE_INVOKING keeps a login bash in the session's folder instead of moving to HOME.
|
|
57
|
+
gitBash && { id: 'git-bash', label: 'Git Bash', path: gitBash, args: ['--login', '-i'], env: { CHERE_INVOKING: '1' } },
|
|
58
|
+
].filter((shell) => shell?.path);
|
|
59
|
+
return { shells, defaultId: shells[0]?.id ?? null };
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function unixShells(env, platform, resolve) {
|
|
63
|
+
const name = (file) => path.posix.basename(file);
|
|
64
|
+
const own = resolve(env.SHELL || '') || resolve(fallbackShell({}, platform));
|
|
65
|
+
const shells = [];
|
|
66
|
+
for (const { id, label } of UNIX_SHELLS) {
|
|
67
|
+
const found = own && name(own) === id ? own : resolve(id);
|
|
68
|
+
if (found) shells.push({ id, label, path: found, args: [], env: {} });
|
|
69
|
+
}
|
|
70
|
+
let defaultId = own ? name(own) : null;
|
|
71
|
+
if (own && !shells.some((shell) => shell.id === defaultId)) {
|
|
72
|
+
if (!/^[A-Za-z0-9][A-Za-z0-9._-]{0,31}$/.test(defaultId)) defaultId = 'login';
|
|
73
|
+
shells.unshift({ id: defaultId, label: name(own), path: own, args: [], env: {} });
|
|
74
|
+
}
|
|
75
|
+
return { shells, defaultId: defaultId ?? shells[0]?.id ?? null };
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/** The installed shells, as { id, label, path, args, env }, and the id of the default one. */
|
|
79
|
+
export function detectShells(env = process.env, platform = process.platform, { exists = isFile, resolve: resolveWith = resolveCommand } = {}) {
|
|
80
|
+
const resolve = (command) => resolveWith(command, env, platform);
|
|
81
|
+
return platform === 'win32' ? windowsShells(env, resolve, exists) : unixShells(env, platform, resolve);
|
|
82
|
+
}
|
package/web/app.js
CHANGED
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
const TOKEN_KEY = 'agentGuild.token';
|
|
5
5
|
const CWD_KEY = 'agentGuild.cwd';
|
|
6
6
|
const ACCOUNTS_KEY = 'agentGuild.accounts';
|
|
7
|
+
const SHELLS_KEY = 'agentGuild.shells';
|
|
7
8
|
const THEME_KEY = 'agentGuild.theme';
|
|
8
9
|
const SKIN_KEY = 'agentGuild.skin';
|
|
9
10
|
const NEWS_SEEN_KEY = 'agentGuild.newsSeen';
|
|
@@ -23,6 +24,7 @@ const state = {
|
|
|
23
24
|
providers: [],
|
|
24
25
|
usage: new Map(),
|
|
25
26
|
accounts: {},
|
|
27
|
+
shellPicks: {},
|
|
26
28
|
stats: null,
|
|
27
29
|
statsFor: new Map(),
|
|
28
30
|
news: null,
|
|
@@ -456,6 +458,51 @@ function selectAccount(provider, id) {
|
|
|
456
458
|
save(ACCOUNTS_KEY, JSON.stringify(state.accounts));
|
|
457
459
|
}
|
|
458
460
|
|
|
461
|
+
/** The shell the user picked for the provider, or null to start its default. */
|
|
462
|
+
function pickedShell(provider) {
|
|
463
|
+
return provider.shells?.find((s) => s.id === state.shellPicks[provider.id] && s.id !== provider.defaultShell) ?? null;
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
function selectedShell(provider) {
|
|
467
|
+
return pickedShell(provider) ?? provider.shells?.find((s) => s.id === provider.defaultShell) ?? null;
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
function selectShell(provider, id) {
|
|
471
|
+
if (id === provider.defaultShell) delete state.shellPicks[provider.id];
|
|
472
|
+
else state.shellPicks[provider.id] = id;
|
|
473
|
+
save(SHELLS_KEY, JSON.stringify(state.shellPicks));
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
function renderShells(card, provider) {
|
|
477
|
+
const host = card.querySelector('.shells');
|
|
478
|
+
const shells = provider.shells || [];
|
|
479
|
+
host.hidden = !provider.available || shells.length < 2;
|
|
480
|
+
if (host.hidden) return host.replaceChildren();
|
|
481
|
+
const selected = selectedShell(provider)?.id;
|
|
482
|
+
const same = host.children.length === shells.length && shells.every((s, i) => host.children[i].dataset.shell === s.id);
|
|
483
|
+
if (!same) {
|
|
484
|
+
host.replaceChildren(...shells.map((shell) => {
|
|
485
|
+
const chip = document.createElement('button');
|
|
486
|
+
chip.type = 'button';
|
|
487
|
+
chip.className = 'account-chip';
|
|
488
|
+
chip.setAttribute('role', 'tab');
|
|
489
|
+
chip.dataset.shell = shell.id;
|
|
490
|
+
chip.addEventListener('click', () => {
|
|
491
|
+
selectShell(provider, shell.id);
|
|
492
|
+
renderShells(card, provider);
|
|
493
|
+
renderUsage(card, provider);
|
|
494
|
+
});
|
|
495
|
+
return chip;
|
|
496
|
+
}));
|
|
497
|
+
}
|
|
498
|
+
shells.forEach((shell, i) => {
|
|
499
|
+
const chip = host.children[i];
|
|
500
|
+
chip.setAttribute('aria-selected', String(shell.id === selected));
|
|
501
|
+
chip.textContent = shell.label;
|
|
502
|
+
chip.title = `Start new sessions in ${shell.label}${shell.id === provider.defaultShell ? ', used unless you pick another' : ''}\n${shell.path}`;
|
|
503
|
+
});
|
|
504
|
+
}
|
|
505
|
+
|
|
459
506
|
function usageFor(provider, account = selectedAccount(provider)) {
|
|
460
507
|
return state.usage.get(`${provider.id}/${account.id}`);
|
|
461
508
|
}
|
|
@@ -559,6 +606,7 @@ function renderProviders() {
|
|
|
559
606
|
renderCopies(node.querySelector('.copies'), provider, node);
|
|
560
607
|
renderVendorLinks(node, provider);
|
|
561
608
|
renderAccounts(node, provider);
|
|
609
|
+
renderShells(node, provider);
|
|
562
610
|
renderUsage(node, provider);
|
|
563
611
|
renderReportingSetup(node, provider);
|
|
564
612
|
renderModelStats(node, provider);
|
|
@@ -685,7 +733,7 @@ function renderUsage(card, provider) {
|
|
|
685
733
|
start.textContent = unsigned ? 'Sign in' : 'New';
|
|
686
734
|
start.title = unsigned
|
|
687
735
|
? `Start a ${provider.tool} session and sign in as the ${account.label} account`
|
|
688
|
-
: `Start a new ${provider.tool} session${provider.accounts?.length > 1 ? ` as the ${account.label} account` : ''}`;
|
|
736
|
+
: `Start a new ${provider.tool} session${provider.accounts?.length > 1 ? ` as the ${account.label} account` : ''}${provider.shells?.length > 1 ? ` in ${selectedShell(provider)?.label}` : ''}`;
|
|
689
737
|
if (!provider.available || !provider.usageSource || !usage) return host.replaceChildren();
|
|
690
738
|
if (usage.error || usage.windows.length === 0) {
|
|
691
739
|
const note = document.createElement('div');
|
|
@@ -1728,7 +1776,7 @@ async function startSession(provider, card, { resume, cwd, account = selectedAcc
|
|
|
1728
1776
|
save(CWD_KEY, working);
|
|
1729
1777
|
card?.classList.add('busy');
|
|
1730
1778
|
try {
|
|
1731
|
-
const body = { providerId: provider.id, account, cwd: cwd || working || undefined, cols: 120, rows: 32, resume };
|
|
1779
|
+
const body = { providerId: provider.id, account, shell: pickedShell(provider)?.id, cwd: cwd || working || undefined, cols: 120, rows: 32, resume };
|
|
1732
1780
|
let session;
|
|
1733
1781
|
try {
|
|
1734
1782
|
({ session } = await api('POST', '/sessions', body));
|
|
@@ -3403,8 +3451,10 @@ $('panel-stop').addEventListener('click', () => {
|
|
|
3403
3451
|
});
|
|
3404
3452
|
$('cwd').value = load(CWD_KEY) || '';
|
|
3405
3453
|
try { state.accounts = JSON.parse(load(ACCOUNTS_KEY)) || {}; } catch { state.accounts = {}; }
|
|
3454
|
+
try { state.shellPicks = JSON.parse(load(SHELLS_KEY)) || {}; } catch { state.shellPicks = {}; }
|
|
3406
3455
|
githubView.accountId = Number(load(GITHUB_ACCOUNT_KEY)) || null;
|
|
3407
3456
|
if (typeof state.accounts !== 'object' || Array.isArray(state.accounts)) state.accounts = {};
|
|
3457
|
+
if (typeof state.shellPicks !== 'object' || Array.isArray(state.shellPicks)) state.shellPicks = {};
|
|
3408
3458
|
setInterval(renderSessions, 30000);
|
|
3409
3459
|
setInterval(tickNews, 30000);
|
|
3410
3460
|
setInterval(() => { if ($('github').open && state.github) renderGitHub(); }, 30000);
|
package/web/index.html
CHANGED
|
@@ -262,6 +262,7 @@
|
|
|
262
262
|
<a class="cloud-link" target="_blank" rel="noopener noreferrer" hidden></a>
|
|
263
263
|
</div>
|
|
264
264
|
<div class="accounts" role="tablist" aria-label="Accounts" hidden></div>
|
|
265
|
+
<div class="shells" role="tablist" aria-label="Shells" hidden></div>
|
|
265
266
|
<div class="usage"></div>
|
|
266
267
|
<div class="model-stats"></div>
|
|
267
268
|
<div class="reporting-row" hidden>
|
package/web/styles.css
CHANGED
|
@@ -109,7 +109,7 @@ main { max-width: 1200px; margin: 0 auto; padding: 26px 16px 56px; display: grid
|
|
|
109
109
|
.provider .state.update-available { color: var(--warn); }
|
|
110
110
|
.provider.unavailable .state { color: var(--muted); }
|
|
111
111
|
.provider .update { color: var(--warn); border-color: var(--warn); }
|
|
112
|
-
.provider .accounts { display: flex; flex-wrap: wrap; gap: 6px; }
|
|
112
|
+
.provider .accounts, .provider .shells { display: flex; flex-wrap: wrap; gap: 6px; }
|
|
113
113
|
.account-chip {
|
|
114
114
|
font: inherit; font-size: 0.75rem; font-weight: 600; padding: 2px 10px; border-radius: 999px; cursor: pointer;
|
|
115
115
|
border: 1px solid var(--border); background: var(--surface-2); color: var(--muted); max-width: 100%;
|