ai-account-switch 1.9.0 → 1.11.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/src/ui-server.js CHANGED
@@ -120,6 +120,14 @@ class UIServer {
120
120
  this.handleUpdateMcpServer(req, res, pathname);
121
121
  } else if (pathname.startsWith('/api/mcp-servers/') && req.method === 'DELETE') {
122
122
  this.handleDeleteMcpServer(req, res, pathname);
123
+ } else if (pathname === '/api/env' && req.method === 'GET') {
124
+ this.handleGetEnv(req, res);
125
+ } else if (pathname === '/api/env' && req.method === 'POST') {
126
+ this.handleSetEnv(req, res);
127
+ } else if (pathname.startsWith('/api/env/') && req.method === 'DELETE') {
128
+ this.handleDeleteEnv(req, res, pathname);
129
+ } else if (pathname === '/api/env/clear' && req.method === 'POST') {
130
+ this.handleClearEnv(req, res);
123
131
  } else {
124
132
  res.writeHead(404, { 'Content-Type': 'application/json' });
125
133
  res.end(JSON.stringify({ error: 'Not found' }));
@@ -451,6 +459,8 @@ class UIServer {
451
459
  return;
452
460
  }
453
461
 
462
+ // Include name in serverData to ensure consistency
463
+ serverData.name = name;
454
464
  this.config.addMcpServer(name, serverData);
455
465
  res.writeHead(200, { 'Content-Type': 'application/json' });
456
466
  res.end(JSON.stringify({ success: true, message: 'MCP server added successfully' }));
@@ -479,6 +489,8 @@ class UIServer {
479
489
  return;
480
490
  }
481
491
 
492
+ // Include name in serverData to ensure consistency
493
+ serverData.name = name;
482
494
  this.config.updateMcpServer(name, serverData);
483
495
 
484
496
  // Sync if server is enabled in current project
@@ -707,6 +719,289 @@ class UIServer {
707
719
  }
708
720
  }
709
721
 
722
+ // Helper functions for env handlers
723
+ getClaudeUserConfigPath() {
724
+ // Use ConfigManager's method for consistency
725
+ const claudeConfigPath = this.config.getClaudeUserConfigPath();
726
+
727
+ // If config exists, return it; otherwise, use default path
728
+ if (claudeConfigPath) {
729
+ return claudeConfigPath;
730
+ }
731
+
732
+ // Fallback to default location
733
+ const path = require('path');
734
+ const home = process.env.HOME || process.env.USERPROFILE;
735
+ if (!home) return null;
736
+
737
+ return path.join(home, '.claude', 'settings.json');
738
+ }
739
+
740
+ readClaudeProjectConfig(projectRoot) {
741
+ const path = require('path');
742
+ const fs = require('fs');
743
+ const claudeConfigFile = path.join(projectRoot, '.claude', 'settings.local.json');
744
+
745
+ if (!fs.existsSync(claudeConfigFile)) {
746
+ return { env: {} };
747
+ }
748
+
749
+ try {
750
+ const data = fs.readFileSync(claudeConfigFile, 'utf8');
751
+ const config = JSON.parse(data);
752
+ // Ensure env property exists
753
+ if (!config.env) {
754
+ config.env = {};
755
+ }
756
+ return config;
757
+ } catch (error) {
758
+ return { env: {} };
759
+ }
760
+ }
761
+
762
+ writeClaudeProjectConfig(claudeConfig, projectRoot) {
763
+ const path = require('path');
764
+ const fs = require('fs');
765
+ const claudeDir = path.join(projectRoot, '.claude');
766
+ const claudeConfigFile = path.join(claudeDir, 'settings.local.json');
767
+
768
+ if (!fs.existsSync(claudeDir)) {
769
+ fs.mkdirSync(claudeDir, { recursive: true });
770
+ }
771
+
772
+ // Read existing config and merge with new env
773
+ let existingConfig = {};
774
+ if (fs.existsSync(claudeConfigFile)) {
775
+ try {
776
+ const data = fs.readFileSync(claudeConfigFile, 'utf8');
777
+ existingConfig = JSON.parse(data);
778
+ } catch (error) {
779
+ // If parsing fails, start fresh
780
+ }
781
+ }
782
+
783
+ // Merge env property
784
+ existingConfig.env = claudeConfig.env || {};
785
+
786
+ fs.writeFileSync(claudeConfigFile, JSON.stringify(existingConfig, null, 2), 'utf8');
787
+ }
788
+
789
+ readClaudeUserConfig() {
790
+ const fs = require('fs');
791
+ const claudeConfigPath = this.getClaudeUserConfigPath();
792
+
793
+ if (!claudeConfigPath || !fs.existsSync(claudeConfigPath)) {
794
+ return { env: {} };
795
+ }
796
+
797
+ try {
798
+ const data = fs.readFileSync(claudeConfigPath, 'utf8');
799
+ const config = JSON.parse(data);
800
+ // Ensure env property exists
801
+ if (!config.env) {
802
+ config.env = {};
803
+ }
804
+ return config;
805
+ } catch (error) {
806
+ return { env: {} };
807
+ }
808
+ }
809
+
810
+ writeClaudeUserConfig(claudeConfig) {
811
+ const fs = require('fs');
812
+ const path = require('path');
813
+ const claudeConfigPath = this.getClaudeUserConfigPath();
814
+
815
+ if (!claudeConfigPath) {
816
+ throw new Error('Could not determine Claude config path');
817
+ }
818
+
819
+ const claudeConfigDir = path.dirname(claudeConfigPath);
820
+ if (!fs.existsSync(claudeConfigDir)) {
821
+ fs.mkdirSync(claudeConfigDir, { recursive: true });
822
+ }
823
+
824
+ // Read existing config and merge with new env
825
+ let existingConfig = {};
826
+ if (fs.existsSync(claudeConfigPath)) {
827
+ try {
828
+ const data = fs.readFileSync(claudeConfigPath, 'utf8');
829
+ existingConfig = JSON.parse(data);
830
+ } catch (error) {
831
+ // If parsing fails, start fresh
832
+ }
833
+ }
834
+
835
+ // Merge env property
836
+ existingConfig.env = claudeConfig.env || {};
837
+
838
+ fs.writeFileSync(claudeConfigPath, JSON.stringify(existingConfig, null, 2), 'utf8');
839
+ }
840
+
841
+ handleGetEnv(req, res) {
842
+ try {
843
+ const url = new URL(req.url, `http://${req.headers.host}`);
844
+ const level = url.searchParams.get('level') || 'all';
845
+
846
+ let result = {
847
+ project: null,
848
+ user: null
849
+ };
850
+
851
+ const projectRoot = this.config.findProjectRoot();
852
+ if (projectRoot && (level === 'all' || level === 'project')) {
853
+ const projectConfig = this.readClaudeProjectConfig(projectRoot);
854
+ result.project = {
855
+ path: projectRoot,
856
+ configPath: require('path').join(projectRoot, '.claude', 'settings.local.json'),
857
+ env: projectConfig.env || {}
858
+ };
859
+ }
860
+
861
+ if (level === 'all' || level === 'user') {
862
+ const userConfig = this.readClaudeUserConfig();
863
+ result.user = {
864
+ configPath: this.getClaudeUserConfigPath(),
865
+ env: userConfig.env || {}
866
+ };
867
+ }
868
+
869
+ res.writeHead(200, { 'Content-Type': 'application/json' });
870
+ res.end(JSON.stringify(result));
871
+ } catch (error) {
872
+ res.writeHead(500, { 'Content-Type': 'application/json' });
873
+ res.end(JSON.stringify({ error: error.message }));
874
+ }
875
+ }
876
+
877
+ handleSetEnv(req, res) {
878
+ let body = '';
879
+ req.on('data', chunk => {
880
+ body += chunk.toString();
881
+ });
882
+
883
+ req.on('end', () => {
884
+ try {
885
+ const data = JSON.parse(body);
886
+ const { key, value, level } = data;
887
+
888
+ if (!key || value === undefined) {
889
+ res.writeHead(400, { 'Content-Type': 'application/json' });
890
+ res.end(JSON.stringify({ error: 'Key and value are required' }));
891
+ return;
892
+ }
893
+
894
+ const targetLevel = level || 'user';
895
+
896
+ if (targetLevel === 'project') {
897
+ const projectRoot = this.config.findProjectRoot();
898
+ if (!projectRoot) {
899
+ res.writeHead(400, { 'Content-Type': 'application/json' });
900
+ res.end(JSON.stringify({ error: 'Not in a project directory' }));
901
+ return;
902
+ }
903
+ const projectConfig = this.readClaudeProjectConfig(projectRoot);
904
+ projectConfig.env = projectConfig.env || {};
905
+ projectConfig.env[key] = value;
906
+ this.writeClaudeProjectConfig(projectConfig, projectRoot);
907
+ } else {
908
+ const userConfig = this.readClaudeUserConfig();
909
+ userConfig.env = userConfig.env || {};
910
+ userConfig.env[key] = value;
911
+ this.writeClaudeUserConfig(userConfig);
912
+ }
913
+
914
+ res.writeHead(200, { 'Content-Type': 'application/json' });
915
+ res.end(JSON.stringify({ success: true, message: 'Environment variable set successfully' }));
916
+ } catch (error) {
917
+ res.writeHead(500, { 'Content-Type': 'application/json' });
918
+ res.end(JSON.stringify({ error: error.message }));
919
+ }
920
+ });
921
+ }
922
+
923
+ handleDeleteEnv(req, res, pathname) {
924
+ const url = new URL(req.url, `http://${req.headers.host}`);
925
+ const level = url.searchParams.get('level') || 'user';
926
+ const key = decodeURIComponent(pathname.split('/api/env/')[1]);
927
+
928
+ try {
929
+ if (!key) {
930
+ res.writeHead(400, { 'Content-Type': 'application/json' });
931
+ res.end(JSON.stringify({ error: 'Key is required' }));
932
+ return;
933
+ }
934
+
935
+ if (level === 'project') {
936
+ const projectRoot = this.config.findProjectRoot();
937
+ if (!projectRoot) {
938
+ res.writeHead(400, { 'Content-Type': 'application/json' });
939
+ res.end(JSON.stringify({ error: 'Not in a project directory' }));
940
+ return;
941
+ }
942
+ const projectConfig = this.readClaudeProjectConfig(projectRoot);
943
+ if (!projectConfig.env || !projectConfig.env[key]) {
944
+ res.writeHead(404, { 'Content-Type': 'application/json' });
945
+ res.end(JSON.stringify({ error: 'Environment variable not found' }));
946
+ return;
947
+ }
948
+ delete projectConfig.env[key];
949
+ this.writeClaudeProjectConfig(projectConfig, projectRoot);
950
+ } else {
951
+ const userConfig = this.readClaudeUserConfig();
952
+ if (!userConfig.env || !userConfig.env[key]) {
953
+ res.writeHead(404, { 'Content-Type': 'application/json' });
954
+ res.end(JSON.stringify({ error: 'Environment variable not found' }));
955
+ return;
956
+ }
957
+ delete userConfig.env[key];
958
+ this.writeClaudeUserConfig(userConfig);
959
+ }
960
+
961
+ res.writeHead(200, { 'Content-Type': 'application/json' });
962
+ res.end(JSON.stringify({ success: true, message: 'Environment variable deleted successfully' }));
963
+ } catch (error) {
964
+ res.writeHead(500, { 'Content-Type': 'application/json' });
965
+ res.end(JSON.stringify({ error: error.message }));
966
+ }
967
+ }
968
+
969
+ handleClearEnv(req, res) {
970
+ let body = '';
971
+ req.on('data', chunk => {
972
+ body += chunk.toString();
973
+ });
974
+
975
+ req.on('end', () => {
976
+ try {
977
+ const data = body ? JSON.parse(body) : {};
978
+ const level = data.level || 'user';
979
+
980
+ if (level === 'project') {
981
+ const projectRoot = this.config.findProjectRoot();
982
+ if (!projectRoot) {
983
+ res.writeHead(400, { 'Content-Type': 'application/json' });
984
+ res.end(JSON.stringify({ error: 'Not in a project directory' }));
985
+ return;
986
+ }
987
+ const projectConfig = this.readClaudeProjectConfig(projectRoot);
988
+ projectConfig.env = {};
989
+ this.writeClaudeProjectConfig(projectConfig, projectRoot);
990
+ } else {
991
+ const userConfig = this.readClaudeUserConfig();
992
+ userConfig.env = {};
993
+ this.writeClaudeUserConfig(userConfig);
994
+ }
995
+
996
+ res.writeHead(200, { 'Content-Type': 'application/json' });
997
+ res.end(JSON.stringify({ success: true, message: 'Environment variables cleared successfully' }));
998
+ } catch (error) {
999
+ res.writeHead(500, { 'Content-Type': 'application/json' });
1000
+ res.end(JSON.stringify({ error: error.message }));
1001
+ }
1002
+ });
1003
+ }
1004
+
710
1005
  getHTMLContent() {
711
1006
  return `<!DOCTYPE html>
712
1007
  <html lang="zh-CN">
@@ -957,12 +1252,120 @@ class UIServer {
957
1252
  cursor: pointer;
958
1253
  }
959
1254
 
1255
+ .view-toggle {
1256
+ display: flex;
1257
+ gap: 5px;
1258
+ background: var(--input-bg);
1259
+ border: 1px solid var(--input-border);
1260
+ border-radius: 5px;
1261
+ padding: 3px;
1262
+ }
1263
+
1264
+ .view-toggle-btn {
1265
+ padding: 8px 15px;
1266
+ border: none;
1267
+ background: transparent;
1268
+ color: var(--text-secondary);
1269
+ cursor: pointer;
1270
+ border-radius: 3px;
1271
+ font-size: 14px;
1272
+ transition: all 0.3s;
1273
+ display: flex;
1274
+ align-items: center;
1275
+ gap: 5px;
1276
+ }
1277
+
1278
+ .view-toggle-btn:hover {
1279
+ background: var(--border-color);
1280
+ }
1281
+
1282
+ .view-toggle-btn.active {
1283
+ background: #4CAF50;
1284
+ color: white;
1285
+ }
1286
+
960
1287
  .accounts-grid {
961
1288
  display: grid;
962
1289
  grid-template-columns: repeat(auto-fill, minmax(350px, 1fr));
963
1290
  gap: 20px;
964
1291
  }
965
1292
 
1293
+ .accounts-list {
1294
+ display: flex;
1295
+ flex-direction: column;
1296
+ gap: 10px;
1297
+ }
1298
+
1299
+ .account-list-item {
1300
+ background: var(--card-bg);
1301
+ border-radius: 8px;
1302
+ padding: 15px 20px;
1303
+ box-shadow: 0 2px 4px var(--card-shadow);
1304
+ transition: all 0.3s;
1305
+ display: flex;
1306
+ align-items: center;
1307
+ justify-content: space-between;
1308
+ border-left: 4px solid transparent;
1309
+ }
1310
+
1311
+ .account-list-item:hover {
1312
+ box-shadow: 0 4px 8px var(--card-shadow-hover);
1313
+ transform: translateX(5px);
1314
+ }
1315
+
1316
+ .account-list-item.type-claude {
1317
+ border-left-color: #1976d2;
1318
+ }
1319
+
1320
+ .account-list-item.type-codex {
1321
+ border-left-color: #7b1fa2;
1322
+ }
1323
+
1324
+ .account-list-item.type-droids {
1325
+ border-left-color: #388e3c;
1326
+ }
1327
+
1328
+ .account-list-item.type-ccr {
1329
+ border-left-color: #ff9800;
1330
+ }
1331
+
1332
+ .account-list-item.type-other {
1333
+ border-left-color: #f57c00;
1334
+ }
1335
+
1336
+ .account-list-left {
1337
+ display: flex;
1338
+ align-items: center;
1339
+ gap: 20px;
1340
+ flex: 1;
1341
+ }
1342
+
1343
+ .account-list-info {
1344
+ display: flex;
1345
+ flex-direction: column;
1346
+ gap: 5px;
1347
+ }
1348
+
1349
+ .account-list-name {
1350
+ font-size: 16px;
1351
+ font-weight: 600;
1352
+ color: var(--text-primary);
1353
+ }
1354
+
1355
+ .account-list-details {
1356
+ display: flex;
1357
+ gap: 15px;
1358
+ align-items: center;
1359
+ font-size: 13px;
1360
+ color: var(--text-secondary);
1361
+ }
1362
+
1363
+ .account-list-right {
1364
+ display: flex;
1365
+ align-items: center;
1366
+ gap: 10px;
1367
+ }
1368
+
966
1369
  .account-card {
967
1370
  background: var(--card-bg);
968
1371
  border-radius: 10px;
@@ -1404,6 +1807,7 @@ class UIServer {
1404
1807
  <div class="tab-navigation">
1405
1808
  <button class="tab-btn active" id="accountsTabBtn" data-i18n="accountsTab">账号管理</button>
1406
1809
  <button class="tab-btn" id="mcpTabBtn" data-i18n="mcpTab">MCP 服务器</button>
1810
+ <button class="tab-btn" id="envTabBtn" data-i18n="envTab">环境变量</button>
1407
1811
  </div>
1408
1812
 
1409
1813
  <!-- Accounts Tab -->
@@ -1422,6 +1826,14 @@ class UIServer {
1422
1826
  <option value="Other" data-i18n="other">其他</option>
1423
1827
  </select>
1424
1828
  </div>
1829
+ <div class="view-toggle">
1830
+ <button class="view-toggle-btn active" id="gridViewBtn" onclick="switchView('grid')" title="块视图">
1831
+ <span>⊞</span>
1832
+ </button>
1833
+ <button class="view-toggle-btn" id="listViewBtn" onclick="switchView('list')" title="列表视图">
1834
+ <span>☰</span>
1835
+ </button>
1836
+ </div>
1425
1837
  <button class="btn btn-primary" onclick="showAddModal()" data-i18n="addAccount">+ 添加账号</button>
1426
1838
  <button class="btn btn-secondary" onclick="exportAccounts()" data-i18n="exportAll">导出全部</button>
1427
1839
  <button class="btn btn-secondary" onclick="document.getElementById('importFile').click()" data-i18n="import">导入</button>
@@ -1451,6 +1863,14 @@ class UIServer {
1451
1863
  <option value="http">http</option>
1452
1864
  </select>
1453
1865
  </div>
1866
+ <div class="view-toggle">
1867
+ <button class="view-toggle-btn active" id="mcpGridViewBtn" onclick="switchMcpView('grid')" title="块视图">
1868
+ <span>⊞</span>
1869
+ </button>
1870
+ <button class="view-toggle-btn" id="mcpListViewBtn" onclick="switchMcpView('list')" title="列表视图">
1871
+ <span>☰</span>
1872
+ </button>
1873
+ </div>
1454
1874
  <button class="btn btn-primary" onclick="showAddMcpModal()" data-i18n="addMcpServer">+ 添加 MCP 服务器</button>
1455
1875
  <button class="btn btn-secondary" onclick="syncMcpConfig()" data-i18n="syncMcp">同步配置</button>
1456
1876
  </div>
@@ -1463,6 +1883,28 @@ class UIServer {
1463
1883
  </div>
1464
1884
  </div>
1465
1885
  <!-- End MCP Tab -->
1886
+
1887
+ <!-- Env Tab -->
1888
+ <div id="envTab" class="tab-content">
1889
+ <div class="controls">
1890
+ <div class="filter-box">
1891
+ <select id="envLevelFilter" onchange="renderEnvVars()">
1892
+ <option value="all" data-i18n="allLevels">所有级别</option>
1893
+ <option value="project" data-i18n="projectLevel">项目级别</option>
1894
+ <option value="user" data-i18n="userLevel">用户级别</option>
1895
+ </select>
1896
+ </div>
1897
+ <button class="btn btn-primary" onclick="showAddEnvModal()" data-i18n="addEnvVar">+ 添加环境变量</button>
1898
+ </div>
1899
+
1900
+ <div id="envContainer" class="env-container"></div>
1901
+ <div id="envEmptyState" class="empty-state hidden">
1902
+ <h2 data-i18n="noEnvVars">还没有环境变量</h2>
1903
+ <p data-i18n="getEnvStarted">开始添加你的第一个环境变量吧</p>
1904
+ <button class="btn btn-primary" onclick="showAddEnvModal()" data-i18n="addEnvVar">+ 添加环境变量</button>
1905
+ </div>
1906
+ </div>
1907
+ <!-- End Env Tab -->
1466
1908
  </div>
1467
1909
 
1468
1910
  <!-- Add/Edit Account Modal -->
@@ -1495,13 +1937,24 @@ class UIServer {
1495
1937
  <!-- Wire API selection for Codex accounts -->
1496
1938
  <div class="form-group" id="wireApiGroup" style="display: none;">
1497
1939
  <label for="wireApi">Wire API 模式</label>
1498
- <select id="wireApi">
1940
+ <select id="wireApi" onchange="toggleEnvKeyField()">
1499
1941
  <option value="chat">chat - HTTP Headers 认证 (OpenAI 兼容)</option>
1500
1942
  <option value="responses">responses - auth.json 认证 (requires_openai_auth)</option>
1943
+ <option value="env">env - 环境变量认证 (Environment Variable)</option>
1501
1944
  </select>
1502
1945
  <small style="color: #666; display: block; margin-top: 5px;">
1503
1946
  chat: API key 存储在 HTTP headers 中<br>
1504
- responses: API key 存储在 ~/.codex/auth.json
1947
+ responses: API key 存储在 ~/.codex/auth.json 中<br>
1948
+ env: API key 从环境变量中读取
1949
+ </small>
1950
+ </div>
1951
+ <!-- Environment variable name for env mode -->
1952
+ <div class="form-group" id="envKeyGroup" style="display: none;">
1953
+ <label for="envKey">环境变量名称</label>
1954
+ <input type="text" id="envKey" placeholder="AIS_USER_API_KEY" pattern="[A-Z_][A-Z0-9_]*">
1955
+ <small style="color: #666; display: block; margin-top: 5px;">
1956
+ 使用前需要执行: export YOUR_VAR_NAME="your-api-key"<br>
1957
+ 变量名必须使用大写字母、数字和下划线
1505
1958
  </small>
1506
1959
  </div>
1507
1960
  <div class="form-group">
@@ -1627,11 +2080,188 @@ class UIServer {
1627
2080
  </div>
1628
2081
  </div>
1629
2082
 
2083
+ <!-- Add/Edit Env Var Modal -->
2084
+ <div id="envModal" class="modal">
2085
+ <div class="modal-content">
2086
+ <div class="modal-header" id="envModalTitle" data-i18n="addEnvVarTitle">添加环境变量</div>
2087
+ <form id="envForm" onsubmit="saveEnvVar(event)">
2088
+ <div class="form-group">
2089
+ <label for="envLevel" data-i18n="envLevel">级别 *</label>
2090
+ <select id="envLevel" required onchange="updateEnvLevelOptions()">
2091
+ <option value="user" data-i18n="userLevel">用户级别 (所有项目)</option>
2092
+ <option value="project" data-i18n="projectLevel">项目级别 (仅当前项目)</option>
2093
+ </select>
2094
+ </div>
2095
+ <div class="form-group">
2096
+ <label for="envKey" data-i18n="envKey">变量名 *</label>
2097
+ <input type="text" id="envKey" required data-i18n-placeholder="envKeyPlaceholder" placeholder="例如: MY_CUSTOM_VAR" pattern="^[A-Z_][A-Z0-9_]*$">
2098
+ </div>
2099
+ <div class="form-group">
2100
+ <label for="envValue" data-i18n="envValue">变量值 *</label>
2101
+ <input type="text" id="envValue" required data-i18n-placeholder="envValuePlaceholder" placeholder="变量值">
2102
+ </div>
2103
+ <div class="form-actions">
2104
+ <button type="submit" class="btn btn-primary" data-i18n="save">保存</button>
2105
+ <button type="button" class="btn btn-secondary" onclick="closeEnvModal()" data-i18n="cancel">取消</button>
2106
+ </div>
2107
+ </form>
2108
+ </div>
2109
+ </div>
2110
+
2111
+ <style>
2112
+ .env-container {
2113
+ display: grid;
2114
+ grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
2115
+ gap: 15px;
2116
+ padding: 10px 0;
2117
+ }
2118
+
2119
+ .env-section {
2120
+ background: var(--card-bg);
2121
+ border-radius: 10px;
2122
+ padding: 20px;
2123
+ box-shadow: 0 2px 8px var(--card-shadow);
2124
+ transition: box-shadow 0.3s ease;
2125
+ }
2126
+
2127
+ .env-section:hover {
2128
+ box-shadow: 0 4px 16px var(--card-shadow-hover);
2129
+ }
2130
+
2131
+ .env-section-header {
2132
+ display: flex;
2133
+ justify-content: space-between;
2134
+ align-items: center;
2135
+ margin-bottom: 15px;
2136
+ padding-bottom: 10px;
2137
+ border-bottom: 1px solid var(--border-color);
2138
+ }
2139
+
2140
+ .env-section-title {
2141
+ font-size: 1.2rem;
2142
+ font-weight: 600;
2143
+ color: var(--text-primary);
2144
+ }
2145
+
2146
+ .env-section-path {
2147
+ font-size: 0.85rem;
2148
+ color: var(--text-tertiary);
2149
+ margin-top: 5px;
2150
+ }
2151
+
2152
+ .env-section-body {
2153
+ margin-top: 15px;
2154
+ }
2155
+
2156
+ .env-section-title {
2157
+ display: flex;
2158
+ align-items: center;
2159
+ gap: 8px;
2160
+ flex-wrap: wrap;
2161
+ }
2162
+
2163
+ .env-section-title h3 {
2164
+ margin: 0;
2165
+ font-size: 1.1rem;
2166
+ font-weight: 600;
2167
+ color: var(--text-primary);
2168
+ max-width: 200px;
2169
+ overflow: hidden;
2170
+ text-overflow: ellipsis;
2171
+ white-space: nowrap;
2172
+ }
2173
+
2174
+ .env-badge {
2175
+ display: inline-block;
2176
+ padding: 3px 8px;
2177
+ border-radius: 4px;
2178
+ font-size: 0.75rem;
2179
+ font-weight: 500;
2180
+ flex-shrink: 0;
2181
+ }
2182
+
2183
+ .env-badge-project {
2184
+ background: #e3f2fd;
2185
+ color: #1976d2;
2186
+ }
2187
+
2188
+ .env-badge-user {
2189
+ background: #f3e5f5;
2190
+ color: #7b1fa2;
2191
+ }
2192
+
2193
+ [data-theme="dark"] .env-badge-project {
2194
+ background: #1565c0;
2195
+ color: #e3f2fd;
2196
+ }
2197
+
2198
+ [data-theme="dark"] .env-badge-user {
2199
+ background: #6a1b9a;
2200
+ color: #f3e5f5;
2201
+ }
2202
+
2203
+ .env-actions {
2204
+ display: flex;
2205
+ gap: 8px;
2206
+ }
2207
+
2208
+ .env-list {
2209
+ display: flex;
2210
+ flex-direction: column;
2211
+ gap: 10px;
2212
+ }
2213
+
2214
+ .env-item {
2215
+ display: flex;
2216
+ justify-content: space-between;
2217
+ align-items: center;
2218
+ padding: 10px;
2219
+ background: var(--input-bg);
2220
+ border-radius: 6px;
2221
+ border: 1px solid var(--border-color);
2222
+ }
2223
+
2224
+ .env-item-info {
2225
+ flex: 1;
2226
+ min-width: 0;
2227
+ }
2228
+
2229
+ .env-item-key {
2230
+ font-weight: 600;
2231
+ color: var(--text-primary);
2232
+ font-size: 0.95rem;
2233
+ }
2234
+
2235
+ .env-item-value {
2236
+ color: var(--text-secondary);
2237
+ font-size: 0.9rem;
2238
+ margin-top: 3px;
2239
+ word-break: break-all;
2240
+ }
2241
+
2242
+ .env-item-actions {
2243
+ display: flex;
2244
+ gap: 8px;
2245
+ }
2246
+
2247
+ .btn-small {
2248
+ padding: 5px 10px;
2249
+ font-size: 0.85rem;
2250
+ }
2251
+
2252
+ .empty-env {
2253
+ text-align: center;
2254
+ padding: 30px;
2255
+ color: var(--text-tertiary);
2256
+ }
2257
+ </style>
2258
+
1630
2259
  <script>
1631
2260
  // Constants for wire API modes (injected from backend)
1632
2261
  const WIRE_API_MODES = {
1633
2262
  CHAT: '${WIRE_API_MODES.CHAT}',
1634
- RESPONSES: '${WIRE_API_MODES.RESPONSES}'
2263
+ RESPONSES: '${WIRE_API_MODES.RESPONSES}',
2264
+ ENV: '${WIRE_API_MODES.ENV}'
1635
2265
  };
1636
2266
  const DEFAULT_WIRE_API = '${DEFAULT_WIRE_API}';
1637
2267
 
@@ -1730,7 +2360,32 @@ class UIServer {
1730
2360
  mcpSyncFailed: 'MCP 配置同步失败',
1731
2361
  confirmDeleteMcp: '确定要删除 MCP 服务器',
1732
2362
  noResults: '没有找到匹配的结果',
1733
- tryDifferentSearch: '尝试使用不同的搜索条件或筛选器'
2363
+ tryDifferentSearch: '尝试使用不同的搜索条件或筛选器',
2364
+ // Env related
2365
+ envTab: '环境变量',
2366
+ allLevels: '所有级别',
2367
+ projectLevel: '项目级别',
2368
+ userLevel: '用户级别',
2369
+ addEnvVar: '+ 添加环境变量',
2370
+ noEnvVars: '还没有环境变量',
2371
+ getEnvStarted: '开始添加你的第一个环境变量吧',
2372
+ addEnvVarTitle: '添加环境变量',
2373
+ editEnvVarTitle: '编辑环境变量',
2374
+ envLevel: '级别 *',
2375
+ envKey: '变量名 *',
2376
+ envKeyPlaceholder: '例如: MY_CUSTOM_VAR',
2377
+ envValue: '变量值 *',
2378
+ envValuePlaceholder: '变量值',
2379
+ envSaveSuccess: '环境变量保存成功',
2380
+ envSaveFailed: '环境变量保存失败',
2381
+ envDeleteSuccess: '环境变量删除成功',
2382
+ envDeleteFailed: '环境变量删除失败',
2383
+ confirmDeleteEnv: '确定要删除环境变量',
2384
+ projectEnvConfig: '项目环境变量',
2385
+ userEnvConfig: '用户环境变量',
2386
+ // Display labels (without *)
2387
+ envValueLabel: '变量值',
2388
+ envLevelLabel: '级别'
1734
2389
  },
1735
2390
  en: {
1736
2391
  title: 'AIS Account Manager',
@@ -1825,12 +2480,39 @@ class UIServer {
1825
2480
  mcpSyncFailed: 'Failed to sync MCP configuration',
1826
2481
  confirmDeleteMcp: 'Are you sure you want to delete MCP server',
1827
2482
  noResults: 'No matching results found',
1828
- tryDifferentSearch: 'Try using different search terms or filters'
2483
+ tryDifferentSearch: 'Try using different search terms or filters',
2484
+ // Env related
2485
+ envTab: 'Environment Variables',
2486
+ allLevels: 'All Levels',
2487
+ projectLevel: 'Project Level',
2488
+ userLevel: 'User Level',
2489
+ addEnvVar: '+ Add Environment Variable',
2490
+ noEnvVars: 'No environment variables yet',
2491
+ getEnvStarted: 'Get started by adding your first environment variable',
2492
+ addEnvVarTitle: 'Add Environment Variable',
2493
+ editEnvVarTitle: 'Edit Environment Variable',
2494
+ envLevel: 'Level *',
2495
+ envKey: 'Variable Name *',
2496
+ envKeyPlaceholder: 'e.g., MY_CUSTOM_VAR',
2497
+ envValue: 'Variable Value *',
2498
+ envValuePlaceholder: 'Variable value',
2499
+ envSaveSuccess: 'Environment variable saved successfully',
2500
+ envSaveFailed: 'Failed to save environment variable',
2501
+ envDeleteSuccess: 'Environment variable deleted successfully',
2502
+ envDeleteFailed: 'Failed to delete environment variable',
2503
+ confirmDeleteEnv: 'Are you sure you want to delete environment variable',
2504
+ projectEnvConfig: 'Project Environment Variables',
2505
+ userEnvConfig: 'User Environment Variables',
2506
+ // Display labels (without *)
2507
+ envValueLabel: 'Variable Value',
2508
+ envLevelLabel: 'Level'
1829
2509
  }
1830
2510
  };
1831
2511
 
1832
2512
  let currentLang = localStorage.getItem('ais-lang') || 'zh';
1833
2513
  let currentTheme = localStorage.getItem('ais-theme') || 'auto';
2514
+ let currentView = localStorage.getItem('ais-view') || 'grid';
2515
+ let currentMcpView = localStorage.getItem('ais-mcp-view') || 'grid';
1834
2516
  let accounts = {};
1835
2517
  let editingAccount = null;
1836
2518
  let envVarCount = 0;
@@ -1862,6 +2544,10 @@ class UIServer {
1862
2544
  document.getElementById('mcpTab').classList.add('active');
1863
2545
  document.getElementById('mcpTabBtn').classList.add('active');
1864
2546
  loadMcpServers();
2547
+ } else if (tabName === 'env') {
2548
+ document.getElementById('envTab').classList.add('active');
2549
+ document.getElementById('envTabBtn').classList.add('active');
2550
+ loadEnvVars();
1865
2551
  }
1866
2552
  }
1867
2553
 
@@ -1873,6 +2559,9 @@ class UIServer {
1873
2559
  document.getElementById('mcpTabBtn').addEventListener('click', function() {
1874
2560
  switchTab('mcp');
1875
2561
  });
2562
+ document.getElementById('envTabBtn').addEventListener('click', function() {
2563
+ switchTab('env');
2564
+ });
1876
2565
  });
1877
2566
 
1878
2567
  // Initialize theme
@@ -1931,10 +2620,75 @@ class UIServer {
1931
2620
  updateLanguage();
1932
2621
  }
1933
2622
 
2623
+ function switchView(view) {
2624
+ currentView = view;
2625
+ localStorage.setItem('ais-view', view);
2626
+
2627
+ // Update button states
2628
+ document.getElementById('gridViewBtn').classList.toggle('active', view === 'grid');
2629
+ document.getElementById('listViewBtn').classList.toggle('active', view === 'list');
2630
+
2631
+ // Update container class
2632
+ const container = document.getElementById('accountsContainer');
2633
+ if (view === 'list') {
2634
+ container.classList.remove('accounts-grid');
2635
+ container.classList.add('accounts-list');
2636
+ } else {
2637
+ container.classList.remove('accounts-list');
2638
+ container.classList.add('accounts-grid');
2639
+ }
2640
+
2641
+ renderAccounts();
2642
+ }
2643
+
2644
+ function switchMcpView(view) {
2645
+ currentMcpView = view;
2646
+ localStorage.setItem('ais-mcp-view', view);
2647
+
2648
+ // Update button states
2649
+ document.getElementById('mcpGridViewBtn').classList.toggle('active', view === 'grid');
2650
+ document.getElementById('mcpListViewBtn').classList.toggle('active', view === 'list');
2651
+
2652
+ // Update container class
2653
+ const container = document.getElementById('mcpServersContainer');
2654
+ if (view === 'list') {
2655
+ container.classList.remove('accounts-grid');
2656
+ container.classList.add('accounts-list');
2657
+ } else {
2658
+ container.classList.remove('accounts-list');
2659
+ container.classList.add('accounts-grid');
2660
+ }
2661
+
2662
+ renderMcpServers();
2663
+ }
2664
+
1934
2665
  // Initialize
1935
2666
  initTheme();
1936
2667
  updateLanguage();
1937
2668
 
2669
+ // Initialize view preferences
2670
+ function initViews() {
2671
+ // Initialize accounts view
2672
+ const accountsContainer = document.getElementById('accountsContainer');
2673
+ if (currentView === 'list') {
2674
+ accountsContainer.classList.remove('accounts-grid');
2675
+ accountsContainer.classList.add('accounts-list');
2676
+ document.getElementById('gridViewBtn').classList.remove('active');
2677
+ document.getElementById('listViewBtn').classList.add('active');
2678
+ }
2679
+
2680
+ // Initialize MCP view
2681
+ const mcpContainer = document.getElementById('mcpServersContainer');
2682
+ if (currentMcpView === 'list') {
2683
+ mcpContainer.classList.remove('accounts-grid');
2684
+ mcpContainer.classList.add('accounts-list');
2685
+ document.getElementById('mcpGridViewBtn').classList.remove('active');
2686
+ document.getElementById('mcpListViewBtn').classList.add('active');
2687
+ }
2688
+ }
2689
+
2690
+ initViews();
2691
+
1938
2692
  async function loadAccounts() {
1939
2693
  try {
1940
2694
  const response = await fetch('/api/accounts');
@@ -1986,7 +2740,36 @@ class UIServer {
1986
2740
  }
1987
2741
 
1988
2742
  emptyState.classList.add('hidden');
1989
- container.innerHTML = filteredAccounts.map(([name, data]) => {
2743
+
2744
+ if (currentView === 'list') {
2745
+ // List view
2746
+ container.innerHTML = filteredAccounts.map(([name, data]) => {
2747
+ const typeClass = data.type ? \`type-\${data.type.toLowerCase()}\` : 'type-other';
2748
+ return \`
2749
+ <div class="account-list-item \${typeClass}">
2750
+ <div class="account-list-left">
2751
+ <div class="account-list-info">
2752
+ <div class="account-list-name">\${name}</div>
2753
+ <div class="account-list-details">
2754
+ <span class="account-type \${typeClass}">\${data.type || 'N/A'}</span>
2755
+ <span>\${t('apiKeyLabel')}: \${maskApiKey(data.apiKey)}</span>
2756
+ \${data.email ? \`<span>\${data.email}</span>\` : ''}
2757
+ \${data.apiUrl ? \`<span>\${data.apiUrl}</span>\` : ''}
2758
+ </div>
2759
+ </div>
2760
+ </div>
2761
+ <div class="account-list-right">
2762
+ <span class="account-status \${data.lastCheck ? data.lastCheck.status : 'unknown'}" id="status_\${name}" title="\${data.lastCheck ? (data.lastCheck.status === 'available' ? '可用' : data.lastCheck.status === 'unstable' ? '不稳定' : '不可用') : '未检查'}"></span>
2763
+ <button class="btn btn-secondary btn-small" onclick="checkAccount('\${name}')" id="checkBtn_\${name}">状态检查</button>
2764
+ <button class="btn btn-secondary btn-small" onclick="editAccount('\${name}')">\${t('edit')}</button>
2765
+ <button class="btn btn-danger btn-small" onclick="deleteAccount('\${name}')">\${t('delete')}</button>
2766
+ </div>
2767
+ </div>
2768
+ \`;
2769
+ }).join('');
2770
+ } else {
2771
+ // Grid view (original)
2772
+ container.innerHTML = filteredAccounts.map(([name, data]) => {
1990
2773
  const typeClass = data.type ? \`type-\${data.type.toLowerCase()}\` : 'type-other';
1991
2774
  return \`
1992
2775
  <div class="account-card \${typeClass}">
@@ -2046,6 +2829,18 @@ class UIServer {
2046
2829
  <div class="info-label">Wire API</div>
2047
2830
  <div class="info-value">\${data.wireApi || (DEFAULT_WIRE_API + ' (default)')}</div>
2048
2831
  </div>
2832
+ \${data.wireApi === 'env' && data.envKey ? \`
2833
+ <div class="account-info">
2834
+ <div class="info-label">环境变量名称</div>
2835
+ <div class="info-value">\${data.envKey}</div>
2836
+ </div>
2837
+ <div class="account-info" style="background: #fff3cd; padding: 8px; border-radius: 4px; margin-top: 5px;">
2838
+ <div class="info-label" style="color: #856404;">使用提示</div>
2839
+ <div class="info-value" style="color: #856404; font-family: monospace; font-size: 12px;">
2840
+ export \${data.envKey}="your-api-key"
2841
+ </div>
2842
+ </div>
2843
+ \` : ''}
2049
2844
  \` : ''}
2050
2845
  \${data.type === 'CCR' && data.ccrConfig ? \`
2051
2846
  <div class="account-info">
@@ -2064,7 +2859,8 @@ class UIServer {
2064
2859
  </div>
2065
2860
  </div>
2066
2861
  \`;
2067
- }).join('');
2862
+ }).join('');
2863
+ }
2068
2864
  }
2069
2865
 
2070
2866
  function maskApiKey(key) {
@@ -2072,6 +2868,15 @@ class UIServer {
2072
2868
  return key.substring(0, 4) + '****' + key.substring(key.length - 4);
2073
2869
  }
2074
2870
 
2871
+ function toggleEnvKeyField() {
2872
+ const wireApi = document.getElementById('wireApi').value;
2873
+ const envKeyGroup = document.getElementById('envKeyGroup');
2874
+
2875
+ if (envKeyGroup) {
2876
+ envKeyGroup.style.display = wireApi === 'env' ? 'block' : 'none';
2877
+ }
2878
+ }
2879
+
2075
2880
  function toggleModelFields() {
2076
2881
  const accountType = document.getElementById('accountType').value;
2077
2882
  const simpleModelGroup = document.getElementById('simpleModelGroup');
@@ -2082,6 +2887,16 @@ class UIServer {
2082
2887
  // Show/hide wire_api field for Codex accounts
2083
2888
  if (wireApiGroup) {
2084
2889
  wireApiGroup.style.display = accountType === 'Codex' ? 'block' : 'none';
2890
+ // Also toggle envKey field if Codex is selected
2891
+ if (accountType === 'Codex') {
2892
+ toggleEnvKeyField();
2893
+ } else {
2894
+ // Hide envKey field for non-Codex accounts
2895
+ const envKeyGroup = document.getElementById('envKeyGroup');
2896
+ if (envKeyGroup) {
2897
+ envKeyGroup.style.display = 'none';
2898
+ }
2899
+ }
2085
2900
  }
2086
2901
 
2087
2902
  if (accountType === 'Codex' || accountType === 'Droids') {
@@ -2153,6 +2968,12 @@ class UIServer {
2153
2968
  // Load wire_api for Codex accounts
2154
2969
  if (account.type === 'Codex') {
2155
2970
  document.getElementById('wireApi').value = account.wireApi || DEFAULT_WIRE_API;
2971
+ // Load envKey for env mode
2972
+ if (account.envKey) {
2973
+ document.getElementById('envKey').value = account.envKey;
2974
+ }
2975
+ // Show/hide envKey field based on wireApi mode
2976
+ toggleEnvKeyField();
2156
2977
  }
2157
2978
 
2158
2979
  // Clear model groups and CCR config
@@ -2423,6 +3244,13 @@ class UIServer {
2423
3244
  const wireApi = document.getElementById('wireApi').value;
2424
3245
  if (wireApi) {
2425
3246
  accountData.wireApi = wireApi;
3247
+ // Add envKey for env mode
3248
+ if (wireApi === 'env') {
3249
+ const envKey = document.getElementById('envKey').value.trim();
3250
+ if (envKey) {
3251
+ accountData.envKey = envKey;
3252
+ }
3253
+ }
2426
3254
  }
2427
3255
  }
2428
3256
  } else if (accountType === 'CCR') {
@@ -2634,6 +3462,225 @@ class UIServer {
2634
3462
  }
2635
3463
  }
2636
3464
 
3465
+ // Environment Variables Functions
3466
+ let envData = { project: null, user: null };
3467
+ let editingEnvVar = null;
3468
+ let editingEnvLevel = null;
3469
+
3470
+ // Mask sensitive value for display
3471
+ function maskEnvValue(key, value) {
3472
+ if (!key || !value) return value;
3473
+
3474
+ // Check if variable name contains sensitive keywords
3475
+ const isSensitive = key.includes('KEY') || key.includes('TOKEN') || key.includes('SECRET') || key.includes('PASSWORD');
3476
+
3477
+ if (!isSensitive) {
3478
+ return value;
3479
+ }
3480
+
3481
+ // For sensitive values, show first 2 + fixed 6 stars + last 2
3482
+ const strValue = String(value);
3483
+ if (strValue.length <= 4) {
3484
+ // If value is too short, show all stars
3485
+ return '*'.repeat(strValue.length);
3486
+ }
3487
+
3488
+ const firstTwo = strValue.substring(0, 2);
3489
+ const lastTwo = strValue.substring(strValue.length - 2);
3490
+
3491
+ return firstTwo + '******' + lastTwo;
3492
+ }
3493
+
3494
+ async function loadEnvVars() {
3495
+ try {
3496
+ const filter = document.getElementById('envLevelFilter');
3497
+ const level = filter ? filter.value : 'all';
3498
+ const response = await fetch(\`/api/env?level=\${level}\`);
3499
+ envData = await response.json();
3500
+ renderEnvVars();
3501
+ } catch (error) {
3502
+ showToast(t('loadFailed'), 'error');
3503
+ }
3504
+ }
3505
+
3506
+ function renderEnvVars() {
3507
+ const container = document.getElementById('envContainer');
3508
+ const emptyState = document.getElementById('envEmptyState');
3509
+ const filter = document.getElementById('envLevelFilter');
3510
+ const levelFilter = filter ? filter.value : 'all';
3511
+
3512
+ let allEnvVars = [];
3513
+
3514
+ // Collect environment variables based on filter
3515
+ if (levelFilter === 'all' || levelFilter === 'project') {
3516
+ if (envData.project && envData.project.env) {
3517
+ Object.entries(envData.project.env).forEach(([key, value]) => {
3518
+ allEnvVars.push({
3519
+ key,
3520
+ value,
3521
+ level: 'project',
3522
+ configPath: envData.project.configPath
3523
+ });
3524
+ });
3525
+ }
3526
+ }
3527
+
3528
+ if (levelFilter === 'all' || levelFilter === 'user') {
3529
+ if (envData.user && envData.user.env) {
3530
+ Object.entries(envData.user.env).forEach(([key, value]) => {
3531
+ allEnvVars.push({
3532
+ key,
3533
+ value,
3534
+ level: 'user',
3535
+ configPath: envData.user.configPath
3536
+ });
3537
+ });
3538
+ }
3539
+ }
3540
+
3541
+ // Show empty state if no variables
3542
+ if (allEnvVars.length === 0) {
3543
+ container.innerHTML = '';
3544
+ emptyState.classList.remove('hidden');
3545
+ return;
3546
+ }
3547
+
3548
+ emptyState.classList.add('hidden');
3549
+
3550
+ // Add masked value to each env var for display
3551
+ allEnvVars.forEach(envVar => {
3552
+ envVar.maskedValue = maskEnvValue(envVar.key, envVar.value);
3553
+ });
3554
+
3555
+ // Render environment variables
3556
+ container.innerHTML = allEnvVars.map(envVar => \`
3557
+ <div class="env-section">
3558
+ <div class="env-section-header">
3559
+ <div class="env-section-title">
3560
+ <h3 title="\${envVar.key}">\${envVar.key}</h3>
3561
+ <span class="env-badge \${envVar.level === 'project' ? 'env-badge-project' : 'env-badge-user'}">
3562
+ \${envVar.level === 'project' ? t('projectEnvConfig') : t('userEnvConfig')}
3563
+ </span>
3564
+ </div>
3565
+ <div class="env-actions">
3566
+ <button class="btn-icon" onclick="editEnvVar('\${envVar.key}', '\${envVar.level}')" title="\${t('edit')}">
3567
+ <svg width="16" height="16" viewBox="0 0 16 16" fill="currentColor">
3568
+ <path d="M12.146.146a.5.5 0 0 1 .708 0l3 3a.5.5 0 0 1 0 .708l-10 10a.5.5 0 0 1-.168.11l-5 2a.5.5 0 0 1-.65-.65l2-5a.5.5 0 0 1 .11-.168l10-10zM11.207 2.5 13.5 4.793 14.793 3.5 12.5 1.207 11.207 2.5zm1.586 3L10.5 3.207 4 9.707V10h.5a.5.5 0 0 1 .5.5v.5h.5a.5.5 0 0 1 .5.5v.5h.293l6.5-6.5zm-9.761 5.175-.106.106-1.528 3.821 3.821-1.528.106-.106A.5.5 0 0 1 5 12.5V12h-.5a.5.5 0 0 1-.5-.5V11h-.5a.5.5 0 0 1-.468-.325z"/>
3569
+ </svg>
3570
+ </button>
3571
+ <button class="btn-icon btn-icon-danger" onclick="deleteEnvVar('\${envVar.key}', '\${envVar.level}')" title="\${t('delete')}">
3572
+ <svg width="16" height="16" viewBox="0 0 16 16" fill="currentColor">
3573
+ <path d="M5.5 5.5A.5.5 0 0 1 6 6v6a.5.5 0 0 1-1 0V6a.5.5 0 0 1 .5-.5zm2.5 0a.5.5 0 0 1 .5.5v6a.5.5 0 0 1-1 0V6a.5.5 0 0 1 .5-.5zm3 .5a.5.5 0 0 0-1 0v6a.5.5 0 0 0 1 0V6z"/>
3574
+ <path fill-rule="evenodd" d="M14.5 3a1 1 0 0 1-1 1H13v9a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V4h-.5a1 1 0 0 1-1-1V2a1 1 0 0 1 1-1H6a1 1 0 0 1 1-1h2a1 1 0 0 1 1 1h3.5a1 1 0 0 1 1 1v1zM4.118 4 4 4.059V13a1 1 0 0 0 1 1h6a1 1 0 0 0 1-1V4.059L11.882 4H4.118zM2.5 3V2h11v1h-11z"/>
3575
+ </svg>
3576
+ </button>
3577
+ </div>
3578
+ </div>
3579
+ <div class="env-section-body">
3580
+ <div class="info-label">\${t('envValueLabel')}</div>
3581
+ <div class="info-value">\${envVar.maskedValue}</div>
3582
+ <div class="info-label" style="margin-top: 8px;">\${t('envLevelLabel')}</div>
3583
+ <div class="info-value">\${envVar.level === 'project' ? t('projectEnvConfig') : t('userEnvConfig')}</div>
3584
+ <div class="info-label" style="margin-top: 8px;">Config</div>
3585
+ <div class="info-value small">\${envVar.configPath}</div>
3586
+ </div>
3587
+ </div>
3588
+ \`).join('');
3589
+ }
3590
+
3591
+ function showAddEnvModal() {
3592
+ editingEnvVar = null;
3593
+ editingEnvLevel = null;
3594
+ document.getElementById('envModalTitle').textContent = t('addEnvVarTitle');
3595
+ document.getElementById('envForm').reset();
3596
+ document.getElementById('envModal').classList.remove('hidden');
3597
+ }
3598
+
3599
+ function editEnvVar(key, level) {
3600
+ editingEnvVar = key;
3601
+ editingEnvLevel = level;
3602
+ document.getElementById('envModalTitle').textContent = t('editEnvVarTitle');
3603
+
3604
+ // Set form values
3605
+ document.getElementById('envKey').value = key;
3606
+ document.getElementById('envKey').disabled = true; // Can't change key when editing
3607
+ document.getElementById('envLevel').value = level;
3608
+ document.getElementById('envLevel').disabled = true; // Can't change level when editing
3609
+
3610
+ // Get the value
3611
+ const envObj = level === 'project' ? envData.project : envData.user;
3612
+ if (envObj && envObj.env && envObj.env[key]) {
3613
+ document.getElementById('envValue').value = envObj.env[key];
3614
+ }
3615
+
3616
+ document.getElementById('envModal').classList.remove('hidden');
3617
+ }
3618
+
3619
+ async function saveEnvVar(event) {
3620
+ event.preventDefault();
3621
+
3622
+ const key = document.getElementById('envKey').value.trim();
3623
+ const value = document.getElementById('envValue').value.trim();
3624
+ const level = document.getElementById('envLevel').value;
3625
+
3626
+ try {
3627
+ const response = await fetch('/api/env', {
3628
+ method: 'POST',
3629
+ headers: { 'Content-Type': 'application/json' },
3630
+ body: JSON.stringify({ key, value, level })
3631
+ });
3632
+
3633
+ const result = await response.json();
3634
+
3635
+ if (response.ok) {
3636
+ showToast(t('envSaveSuccess'), 'success');
3637
+ closeEnvModal();
3638
+ loadEnvVars();
3639
+ } else {
3640
+ showToast(t('envSaveFailed') + ': ' + result.error, 'error');
3641
+ }
3642
+ } catch (error) {
3643
+ showToast(t('envSaveFailed') + ': ' + error.message, 'error');
3644
+ }
3645
+ }
3646
+
3647
+ function closeEnvModal() {
3648
+ document.getElementById('envModal').classList.add('hidden');
3649
+ document.getElementById('envForm').reset();
3650
+ document.getElementById('envKey').disabled = false;
3651
+ document.getElementById('envLevel').disabled = false;
3652
+ editingEnvVar = null;
3653
+ editingEnvLevel = null;
3654
+ }
3655
+
3656
+ async function deleteEnvVar(key, level) {
3657
+ if (!confirm(t('confirmDeleteEnv') + ': ' + key + '?')) {
3658
+ return;
3659
+ }
3660
+
3661
+ try {
3662
+ const response = await fetch(\`/api/env/\${encodeURIComponent(key)}?level=\${level}\`, {
3663
+ method: 'DELETE'
3664
+ });
3665
+
3666
+ const result = await response.json();
3667
+
3668
+ if (response.ok) {
3669
+ showToast(t('envDeleteSuccess'), 'success');
3670
+ loadEnvVars();
3671
+ } else {
3672
+ showToast(t('envDeleteFailed') + ': ' + result.error, 'error');
3673
+ }
3674
+ } catch (error) {
3675
+ showToast(t('envDeleteFailed') + ': ' + error.message, 'error');
3676
+ }
3677
+ }
3678
+
3679
+ function updateEnvLevelOptions() {
3680
+ // Can be used to update options based on context
3681
+ // Currently no dynamic updates needed
3682
+ }
3683
+
2637
3684
  // MCP Functions
2638
3685
  async function loadMcpServers() {
2639
3686
  try {
@@ -2683,7 +3730,43 @@ class UIServer {
2683
3730
  }
2684
3731
 
2685
3732
  emptyState.classList.add('hidden');
2686
- container.innerHTML = filteredServers.map(([name, data]) => {
3733
+
3734
+ if (currentMcpView === 'list') {
3735
+ // List view
3736
+ container.innerHTML = filteredServers.map(([name, data]) => {
3737
+ const isEnabled = enabledMcpServers.includes(name);
3738
+ const statusClass = isEnabled ? 'available' : 'unknown';
3739
+ const statusText = isEnabled ? t('mcpEnabled') : t('mcpDisabled');
3740
+
3741
+ return \`
3742
+ <div class="account-list-item">
3743
+ <div class="account-list-left">
3744
+ <div class="account-list-info">
3745
+ <div class="account-list-name">\${name}</div>
3746
+ <div class="account-list-details">
3747
+ <span class="account-type type-other">\${data.type}</span>
3748
+ \${data.description ? \`<span>\${data.description}</span>\` : ''}
3749
+ \${data.command ? \`<span>\${data.command}</span>\` : ''}
3750
+ \${data.url ? \`<span>\${data.url}</span>\` : ''}
3751
+ </div>
3752
+ </div>
3753
+ </div>
3754
+ <div class="account-list-right">
3755
+ <span class="account-status \${statusClass}" title="\${statusText}"></span>
3756
+ <button class="btn btn-secondary btn-small" onclick="editMcpServer('\${name}')">\${t('edit')}</button>
3757
+ <button class="btn btn-secondary btn-small" onclick="testMcpServer('\${name}')">\${t('testMcp')}</button>
3758
+ \${isEnabled ?
3759
+ \`<button class="btn btn-secondary btn-small" onclick="disableMcpServer('\${name}')">\${t('disableMcp')}</button>\` :
3760
+ \`<button class="btn btn-primary btn-small" onclick="enableMcpServer('\${name}')">\${t('enableMcp')}</button>\`
3761
+ }
3762
+ <button class="btn btn-danger btn-small" onclick="deleteMcpServer('\${name}')">\${t('delete')}</button>
3763
+ </div>
3764
+ </div>
3765
+ \`;
3766
+ }).join('');
3767
+ } else {
3768
+ // Grid view (original)
3769
+ container.innerHTML = filteredServers.map(([name, data]) => {
2687
3770
  const isEnabled = enabledMcpServers.includes(name);
2688
3771
  const statusClass = isEnabled ? 'available' : 'unknown';
2689
3772
  const statusText = isEnabled ? t('mcpEnabled') : t('mcpDisabled');
@@ -2730,7 +3813,8 @@ class UIServer {
2730
3813
  </div>
2731
3814
  </div>
2732
3815
  \`;
2733
- }).join('');
3816
+ }).join('');
3817
+ }
2734
3818
  }
2735
3819
 
2736
3820
  function showAddMcpModal() {