codexmate 0.1.12 → 0.1.14

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 CHANGED
@@ -49,7 +49,7 @@
49
49
 
50
50
  Have you ever felt overwhelmed by managing multiple local AI agents? Each has its own config format, session storage, and skills directory.
51
51
 
52
- **Codex Mate** offers a unified control plane to bring order to the chaos. It's a local-first CLI + Web UI designed to manage [Codex](https://github.com/openai/codex), [Claude Code](https://github.com/anthropic-ai/claude-code), [OpenCode](https://opencode.ai/), KiloCode, and [OpenClaw](https://github.com/moeru-ai/openclaw) seamlessly.
52
+ **Codex Mate** offers a unified control plane to bring order to the chaos. It's a local-first CLI + Web UI designed to manage [Codex](https://github.com/openai/codex), [Claude Code](https://github.com/anthropic-ai/claude-code), [OpenCode](https://opencode.ai/), KiloCode, [OpenClaw](https://github.com/moeru-ai/openclaw), and Pi seamlessly.
53
53
 
54
54
  ### What's So Special?
55
55
 
@@ -59,6 +59,7 @@ Unlike simple wrappers, Codex Mate acts as a **Local Agent Bridge**:
59
59
  - **Claude Provider Bridge**: Connect Claude Code to OpenAI Chat Completions-compatible providers and Ollama through the built-in local Claude-compatible proxy.
60
60
  - **OpenCode Provider Control**: Manage OpenCode provider/model selection with a CodexMate-owned provider store under `~/.codexmate`, projecting only the active provider into native OpenCode config to avoid polluting or deleting user-owned settings.
61
61
  - **KiloCode Config Bridge**: Configure KiloCode providers from the CLI or Web UI, writing to `~/.config/kilo/kilo.jsonc` while preserving stored API keys.
62
+ - **Pi Provider Config**: Manage Pi providers from the Web UI with lossless round-trip writes to `~/.pi/agent/models.json`, auto-fetched remote model lists, and one-click default provider/model switching via `~/.pi/agent/settings.json`.
62
63
  - **Provider Health Cleanup**: Probe local Codex and Claude provider routes, surface failed configs in one modal, and bulk-clean selected broken providers without touching healthy or protected entries.
63
64
  - **Skills Marketplace**: A local-first market to share and import skills between different agent apps.
64
65
  - **Prompt File Editor**: Unified editor for global and project-level `CLAUDE.md` and `AGENTS.md` with auto-detection of project paths, plus a shared preset pool for reusable prompts.
@@ -70,7 +71,7 @@ Unlike simple wrappers, Codex Mate acts as a **Local Agent Bridge**:
70
71
 
71
72
  | Feature | Status | Description |
72
73
  | --- | --- | --- |
73
- | **Provider Management** | ✅ | Switch providers/models for Codex, Claude, OpenCode, KiloCode, and OpenClaw |
74
+ | **Provider Management** | ✅ | Switch providers/models for Codex, Claude, OpenCode, KiloCode, OpenClaw, and Pi |
74
75
  | **Live Agent Sync** | ✅ | Real-time monitoring of Codex/Claude config & status |
75
76
  | **Session Browser** | ✅ | Search, preview, filter, and export sessions across Codex, Claude Code, Gemini CLI, CodeBuddy Code, and Pi |
76
77
  | **Usage Analytics** | ✅ | Visualize message trends and top projects |
@@ -80,6 +81,7 @@ Unlike simple wrappers, Codex Mate acts as a **Local Agent Bridge**:
80
81
  | **Claude Provider Bridge** | ✅ | Connect Claude Code to OpenAI Chat Completions-compatible providers and Ollama via the built-in Claude-compatible proxy |
81
82
  | **OpenCode Provider Store** | ✅ | Keep multiple OpenCode providers in `~/.codexmate` while projecting only the selected provider to native OpenCode config |
82
83
  | **KiloCode Config Bridge** | ✅ | Configure KiloCode through `codexmate kilo config` or the Web UI, writing provider URL/model/API key data to `~/.config/kilo/kilo.jsonc` |
84
+ | **Pi Provider Config** | ✅ | Manage Pi providers in the Web UI writing to `~/.pi/agent/models.json` (unknown fields preserved) and switch the default provider/model in `~/.pi/agent/settings.json` |
83
85
  | **Provider Health Check** | ✅ | Probe local Codex/Claude provider routes, highlight failed configs, and bulk-remove selected broken providers safely |
84
86
  | **Prompt Templates** | ✅ | Reusable prompt plugins with variables |
85
87
  | **Prompt File Editor** | ✅ | Edit global and project-level CLAUDE.md / AGENTS.md with auto-detect, path switching, and a shared preset pool. Applying a preset only updates the editor; save manually to write the file. |
@@ -133,7 +135,7 @@ curl -fsSL https://raw.githubusercontent.com/SakuraByteCore/codexmate/main/scrip
133
135
  - **KiloCode**: `npm install -g @kilocode/cli` (`kilo` / `kilocode`)
134
136
  - **OpenCode**: install from the [official OpenCode docs](https://opencode.ai/)
135
137
  - **OpenClaw**: see the [OpenClaw repository](https://github.com/moeru-ai/openclaw)
136
- - **Pi**: sessions are discovered from `~/.pi/agent/sessions`
138
+ - **Pi**: sessions are discovered from `~/.pi/agent/sessions`; provider config lives in `~/.pi/agent/models.json` and the default provider/model in `~/.pi/agent/settings.json`
137
139
 
138
140
  ---
139
141
 
package/README.vi.md CHANGED
@@ -41,7 +41,7 @@
41
41
 
42
42
  Bạn có bao giờ cảm thấy rối khi phải quản lý nhiều AI agent cục bộ? Mỗi tool lại có định dạng cấu hình, nơi lưu phiên và thư mục skill riêng.
43
43
 
44
- **Codex Mate** cung cấp một trung tâm điều khiển thống nhất để giải quyết sự hỗn loạn đó. Đây là CLI + Web UI ưu tiên cục bộ, được thiết kế để quản lý [Codex](https://github.com/openai/codex), [Claude Code](https://github.com/anthropic-ai/claude-code), [OpenCode](https://opencode.ai/), KiloCode và [OpenClaw](https://github.com/moeru-ai/openclaw) một cách liền mạch.
44
+ **Codex Mate** cung cấp một trung tâm điều khiển thống nhất để giải quyết sự hỗn loạn đó. Đây là CLI + Web UI ưu tiên cục bộ, được thiết kế để quản lý [Codex](https://github.com/openai/codex), [Claude Code](https://github.com/anthropic-ai/claude-code), [OpenCode](https://opencode.ai/), KiloCode, [OpenClaw](https://github.com/moeru-ai/openclaw) và Pi một cách liền mạch.
45
45
 
46
46
  ### Điểm nổi bật
47
47
 
@@ -58,7 +58,8 @@ Khác với các wrapper đơn giản, Codex Mate hoạt động như một **Ag
58
58
 
59
59
  | Tính năng | Trạng thái | Mô tả |
60
60
  | --- | --- | --- |
61
- | **Quản lý Provider** | ✅ | Chuyển đổi provider/model cho Codex, Claude, OpenCode, KiloCode và OpenClaw |
61
+ | **Quản lý Provider** | ✅ | Chuyển đổi provider/model cho Codex, Claude, OpenCode, KiloCode, OpenClaw và Pi |
62
+ | **Cấu hình Provider Pi** | ✅ | Quản lý provider Pi trong Web UI, ghi vào `~/.pi/agent/models.json` (giữ nguyên các trường không nhận dạng được) và chuyển đổi provider/model mặc định trong `~/.pi/agent/settings.json` |
62
63
  | **Đồng bộ Agent trực tiếp** | ✅ | Giám sát cấu hình & trạng thái Codex/Claude theo thời gian thực |
63
64
  | **Trình duyệt phiên** | ✅ | Liệt kê, xem trước, lọc và xuất phiên (Codex/Claude/Gemini/CodeBuddy/Pi) |
64
65
  | **Phân tích sử dụng** | ✅ | Trực quan hóa xu hướng tin nhắn và dự án nổi bật |
@@ -118,7 +119,7 @@ curl -fsSL https://raw.githubusercontent.com/SakuraByteCore/codexmate/main/scrip
118
119
  - **KiloCode**: `npm install -g @kilocode/cli` (`kilo` / `kilocode`)
119
120
  - **OpenCode**: xem [tài liệu chính thức OpenCode](https://opencode.ai/)
120
121
  - **OpenClaw**: xem [kho lưu trữ OpenClaw](https://github.com/moeru-ai/openclaw)
121
- - **Pi**: phiên làm việc được phát hiện từ `~/.pi/agent/sessions`
122
+ - **Pi**: phiên làm việc được phát hiện từ `~/.pi/agent/sessions`; cấu hình provider nằm ở `~/.pi/agent/models.json`, provider/model mặc định nằm ở `~/.pi/agent/settings.json`
122
123
 
123
124
  ---
124
125
 
package/README.zh.md CHANGED
@@ -48,7 +48,7 @@
48
48
 
49
49
  你是否曾因管理多个本地 AI 智能体而感到疲惫?每个工具都有自己的配置格式、会话存储和 Skills 目录。
50
50
 
51
- **Codex Mate** 提供了一个统一的控制平面,让混乱重归有序。这是一个本地优先的 CLI + Web UI,旨在无缝管理 [Codex](https://github.com/openai/codex)、[Claude Code](https://github.com/anthropic-ai/claude-code)、[OpenCode](https://opencode.ai/)、KiloCode 和 [OpenClaw](https://github.com/moeru-ai/openclaw)。
51
+ **Codex Mate** 提供了一个统一的控制平面,让混乱重归有序。这是一个本地优先的 CLI + Web UI,旨在无缝管理 [Codex](https://github.com/openai/codex)、[Claude Code](https://github.com/anthropic-ai/claude-code)、[OpenCode](https://opencode.ai/)、KiloCode、[OpenClaw](https://github.com/moeru-ai/openclaw) 与 Pi。
52
52
 
53
53
  ### 有什么独特之处?
54
54
 
@@ -57,6 +57,7 @@
57
57
  - **OpenAI 兼容桥接**:通过归一化 Responses API,让 Codex 能够与任何支持 OpenAI 格式的 UI 配合使用;内建 Codex 转换会补齐并规范化 `User-Agent`、`Version`、`OpenAI-Beta`、`Originator` 等 Codex 指纹,对上游伪装为官方 Codex CLI 请求。
58
58
  - **Claude Provider 桥接**:通过内建本地 Claude 兼容代理,让 Claude Code 接入 OpenAI Chat Completions 兼容 provider 与 Ollama。
59
59
  - **OpenCode Provider 控制**:在 `~/.codexmate` 下维护 CodexMate 自有的 OpenCode 多 provider 存储,只将当前选中的 provider 投影到 OpenCode 原生配置,避免污染或误删用户已有配置。
60
+ - **Pi Provider 配置**:在 Web UI 中管理 Pi 供应商,写入 `~/.pi/agent/models.json` 时无损保留未知字段,支持自动拉取远端模型列表,并可一键切换 `~/.pi/agent/settings.json` 中的默认供应商/模型。
60
61
  - **Provider 健康清理**:探测本地 Codex 与 Claude provider 路由,在同一个弹窗里集中展示失败配置,并批量清理已选坏配置,不影响正常或受保护项。
61
62
  - **Skills 市场**:本地优先的市场,支持在不同的智能体应用之间共享和导入 Skills。
62
63
  - **提示词文件编辑器**:统一编辑全局和项目级 `CLAUDE.md` 与 `AGENTS.md`,支持项目路径自动检测,并提供通用预设池复用 prompt。
@@ -68,7 +69,7 @@
68
69
 
69
70
  | 特性 | 状态 | 描述 |
70
71
  | --- | --- | --- |
71
- | **Provider 管理** | ✅ | 切换 Codex、Claude、OpenCode、KiloCode 和 OpenClaw 的 provider/model |
72
+ | **Provider 管理** | ✅ | 切换 Codex、Claude、OpenCode、KiloCode、OpenClaw 与 Pi 的 provider/model |
72
73
  | **状态实时同步** | ✅ | 实时感知 Codex/Claude 的配置与运行状态变更 |
73
74
  | **会话浏览器** | ✅ | 跨 Codex、Claude Code、Gemini CLI、CodeBuddy Code 与 Pi 的本地会话进行检索、预览、筛选与导出 |
74
75
  | **Usage 统计** | ✅ | 可视化消息趋势与热门项目统计 |
@@ -77,6 +78,7 @@
77
78
  | **OpenAI 桥接** | ✅ | 将 Codex Responses API 转换为标准 OpenAI 格式,并在内建转换中附加/规范化 Codex 指纹 |
78
79
  | **Claude Provider 桥接** | ✅ | 通过内建 Claude 兼容代理,让 Claude Code 接入 OpenAI Chat Completions 兼容 provider 与 Ollama |
79
80
  | **OpenCode Provider 存储** | ✅ | 在 `~/.codexmate` 中保留多个 OpenCode provider,只将当前选中的 provider 投影到 OpenCode 原生配置 |
81
+ | **Pi Provider 配置** | ✅ | 在 Web UI 中管理 Pi 供应商,写入 `~/.pi/agent/models.json`(未知字段无损保留),并切换 `~/.pi/agent/settings.json` 中的默认供应商/模型 |
80
82
  | **Provider 健康检查** | ✅ | 探测本地 Codex/Claude provider 路由,标记失败配置,并安全批量移除已选坏配置 |
81
83
  | **提示词模板** | ✅ | 支持变量的可复用提示词插件 |
82
84
  | **提示词文件编辑器** | ✅ | 编辑全局和项目级 CLAUDE.md / AGENTS.md,支持自动检测、路径切换与通用预设池。应用预设只更新编辑器,需手动保存才会写入文件。 |
@@ -130,7 +132,7 @@ curl -fsSL https://raw.githubusercontent.com/SakuraByteCore/codexmate/main/scrip
130
132
  - **KiloCode**: `npm install -g @kilocode/cli`(`kilo` / `kilocode`)
131
133
  - **OpenCode**: 参照 [OpenCode 官方文档](https://opencode.ai/) 安装
132
134
  - **OpenClaw**: 参见 [OpenClaw 仓库](https://github.com/moeru-ai/openclaw)
133
- - **Pi**: 会话从 `~/.pi/agent/sessions` 自动发现
135
+ - **Pi**: 会话从 `~/.pi/agent/sessions` 自动发现;供应商配置位于 `~/.pi/agent/models.json`,默认供应商/模型位于 `~/.pi/agent/settings.json`
134
136
 
135
137
  ---
136
138
 
package/cli.js CHANGED
@@ -891,8 +891,8 @@ function isPlainObject(value) {
891
891
  return !!value && typeof value === 'object' && !Array.isArray(value);
892
892
  }
893
893
 
894
- const TOOL_CONFIG_PERMISSION_TARGETS = new Set(['codex', 'claude', 'opencode', 'kilocode', 'openclaw']);
895
- const TOOL_CONFIG_PERMISSION_DEFAULTS = Object.freeze({ codex: false, claude: false, opencode: false, kilocode: false, openclaw: false });
894
+ const TOOL_CONFIG_PERMISSION_TARGETS = new Set(['codex', 'claude', 'opencode', 'kilocode', 'openclaw', 'pi']);
895
+ const TOOL_CONFIG_PERMISSION_DEFAULTS = Object.freeze({ codex: false, claude: false, opencode: false, kilocode: false, openclaw: false, pi: false });
896
896
  let toolConfigWriteGuardDepth = 0;
897
897
 
898
898
  function enterToolConfigWriteGuard() {
@@ -921,7 +921,8 @@ function normalizeToolConfigPermissions(value) {
921
921
  claude: source.claude === true,
922
922
  opencode: source.opencode === true,
923
923
  kilocode: source.kilocode === true,
924
- openclaw: source.openclaw === true
924
+ openclaw: source.openclaw === true,
925
+ pi: source.pi === true
925
926
  };
926
927
  }
927
928
 
@@ -979,7 +980,7 @@ function normalizeMainTabPreference(value) {
979
980
 
980
981
  function normalizeConfigModePreference(value) {
981
982
  const normalized = typeof value === 'string' ? value.trim().toLowerCase() : '';
982
- return ['codex', 'claude', 'openclaw', 'opencode', 'kilocode'].includes(normalized) ? normalized : 'codex';
983
+ return ['codex', 'claude', 'openclaw', 'opencode', 'kilocode', 'pi'].includes(normalized) ? normalized : 'codex';
983
984
  }
984
985
 
985
986
  function normalizeUsageTimeRangePreference(value) {
@@ -1203,9 +1204,123 @@ function getApiToolConfigWriteTarget(action) {
1203
1204
  if (opencodeWriteActions.has(name)) return 'opencode';
1204
1205
  if (kilocodeWriteActions.has(name)) return 'kilocode';
1205
1206
  if (openclawWriteActions.has(name)) return 'openclaw';
1207
+ const piWriteActions = new Set([
1208
+ 'write-pi-models',
1209
+ 'write-pi-settings'
1210
+ ]);
1211
+ if (piWriteActions.has(name)) return 'pi';
1206
1212
  return '';
1207
1213
  }
1208
1214
 
1215
+ function getPiAgentDir() {
1216
+ const homeDir = (typeof os.homedir === 'function' ? os.homedir() : null) || process.env.HOME || process.env.USERPROFILE || '';
1217
+ return path.join(homeDir, '.pi', 'agent');
1218
+ }
1219
+
1220
+ function readPiModels(params = {}) {
1221
+ try {
1222
+ const dir = getPiAgentDir();
1223
+ const filePath = path.join(dir, 'models.json');
1224
+ if (!fs.existsSync(filePath)) {
1225
+ return { providers: {} };
1226
+ }
1227
+ const raw = fs.readFileSync(filePath, 'utf-8');
1228
+ const data = JSON.parse(raw);
1229
+ const providers = (data && typeof data === 'object' && data.providers && typeof data.providers === 'object')
1230
+ ? data.providers
1231
+ : {};
1232
+ return { providers };
1233
+ } catch (e) {
1234
+ return { error: e && e.message ? e.message : '读取 Pi models.json 失败' };
1235
+ }
1236
+ }
1237
+
1238
+ function writePiModels(params = {}) {
1239
+ const providers = params && typeof params.providers === 'object' ? params.providers : null;
1240
+ if (providers === null) {
1241
+ return { error: 'providers 不能为空' };
1242
+ }
1243
+ try {
1244
+ const dir = getPiAgentDir();
1245
+ fs.mkdirSync(dir, { recursive: true });
1246
+ const filePath = path.join(dir, 'models.json');
1247
+ let existing = {};
1248
+ if (fs.existsSync(filePath)) {
1249
+ try {
1250
+ existing = JSON.parse(fs.readFileSync(filePath, 'utf-8'));
1251
+ } catch (_) {
1252
+ existing = {};
1253
+ }
1254
+ }
1255
+ existing.providers = providers;
1256
+ fs.writeFileSync(filePath, JSON.stringify(existing, null, 2), 'utf-8');
1257
+ return { success: true };
1258
+ } catch (e) {
1259
+ return { error: e && e.message ? e.message : '写入 Pi models.json 失败' };
1260
+ }
1261
+ }
1262
+
1263
+ async function fetchPiRemoteModels(params = {}) {
1264
+ const baseUrl = typeof params.baseUrl === 'string' ? params.baseUrl.trim() : '';
1265
+ const apiKey = typeof params.apiKey === 'string' ? params.apiKey.trim() : '';
1266
+ if (!baseUrl || !isValidHttpUrl(baseUrl)) {
1267
+ return { error: 'baseUrl 无效', models: [] };
1268
+ }
1269
+ try {
1270
+ const result = await fetchModelsFromBaseUrl(baseUrl, apiKey);
1271
+ const models = result && Array.isArray(result.models)
1272
+ ? [...new Set(result.models.filter((id) => typeof id === 'string' && id !== ''))].sort()
1273
+ : [];
1274
+ if (models.length > 0) return { models };
1275
+ return { error: (result && result.error) || '未获取到可用模型', models: [] };
1276
+ } catch (e) {
1277
+ return { error: e && e.message ? e.message : '请求失败', models: [] };
1278
+ }
1279
+ }
1280
+
1281
+ function readPiSettings(params = {}) {
1282
+ try {
1283
+ const dir = getPiAgentDir();
1284
+ const filePath = path.join(dir, 'settings.json');
1285
+ if (!fs.existsSync(filePath)) {
1286
+ return { settings: {} };
1287
+ }
1288
+ const raw = fs.readFileSync(filePath, 'utf-8');
1289
+ const data = JSON.parse(raw);
1290
+ return { settings: data || {} };
1291
+ } catch (e) {
1292
+ return { error: e && e.message ? e.message : '读取 Pi settings.json 失败' };
1293
+ }
1294
+ }
1295
+
1296
+ function writePiSettings(params = {}) {
1297
+ const updates = {};
1298
+ if (typeof (params && params.defaultProvider) === 'string') updates.defaultProvider = params.defaultProvider;
1299
+ if (typeof (params && params.defaultModel) === 'string') updates.defaultModel = params.defaultModel;
1300
+ if (Object.keys(updates).length === 0) {
1301
+ return { error: '缺少可写入的设置项' };
1302
+ }
1303
+ try {
1304
+ const dir = getPiAgentDir();
1305
+ fs.mkdirSync(dir, { recursive: true });
1306
+ const filePath = path.join(dir, 'settings.json');
1307
+ let existing = {};
1308
+ if (fs.existsSync(filePath)) {
1309
+ try {
1310
+ existing = JSON.parse(fs.readFileSync(filePath, 'utf-8'));
1311
+ } catch (_) {
1312
+ existing = {};
1313
+ }
1314
+ if (!existing || typeof existing !== 'object' || Array.isArray(existing)) existing = {};
1315
+ }
1316
+ const updated = { ...existing, ...updates };
1317
+ fs.writeFileSync(filePath, JSON.stringify(updated, null, 2), 'utf-8');
1318
+ return { success: true, settings: updated };
1319
+ } catch (e) {
1320
+ return { error: e && e.message ? e.message : '写入 Pi settings.json 失败' };
1321
+ }
1322
+ }
1323
+
1209
1324
  function setToolConfigPermission(params = {}) {
1210
1325
  const target = normalizeToolConfigTarget(params && params.target);
1211
1326
  if (!target) return { error: '未知配置对象' };
@@ -13173,6 +13288,26 @@ function createWebServer({ htmlPath, assetsDir, webDir, host, port, openBrowser
13173
13288
  };
13174
13289
  break;
13175
13290
  }
13291
+ case 'read-pi-models': {
13292
+ result = readPiModels(params || {});
13293
+ break;
13294
+ }
13295
+ case 'write-pi-models': {
13296
+ result = writePiModels(params || {});
13297
+ break;
13298
+ }
13299
+ case 'read-pi-settings': {
13300
+ result = readPiSettings(params || {});
13301
+ break;
13302
+ }
13303
+ case 'write-pi-settings': {
13304
+ result = writePiSettings(params || {});
13305
+ break;
13306
+ }
13307
+ case 'fetch-pi-remote-models': {
13308
+ result = await fetchPiRemoteModels(params || {});
13309
+ break;
13310
+ }
13176
13311
  case 'install-status':
13177
13312
  result = buildInstallStatusReport();
13178
13313
  break;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "codexmate",
3
- "version": "0.1.12",
3
+ "version": "0.1.14",
4
4
  "description": "Codex/Claude Code/OpenClaw 配置、会话与任务编排 CLI + Web 工具",
5
5
  "main": "cli.js",
6
6
  "bin": {
package/web-ui/app.js CHANGED
@@ -26,6 +26,35 @@ document.addEventListener('DOMContentLoaded', () => {
26
26
  }
27
27
 
28
28
  const { createApp } = Vue;
29
+ const showFatalErrorOverlay = (label, message, stack, extra) => {
30
+ try {
31
+ const target = document.querySelector('#app') || document.body;
32
+ const pre = document.createElement('pre');
33
+ pre.style.cssText = 'position:fixed;bottom:0;left:0;right:0;background:#900;color:#fff;padding:12px;white-space:pre-wrap;font-size:12px;z-index:9999;';
34
+ pre.textContent = '[' + label + '] ' + (message || '') + '\n' + (stack || '') + (extra ? '\n' + extra : '');
35
+ target.appendChild(pre);
36
+ } catch (_) {}
37
+ };
38
+ window.addEventListener('error', (event) => {
39
+ console.error('[window error]', event.message, event.filename, event.lineno, event.colno, event.error);
40
+ try {
41
+ const target = document.querySelector('#app') || document.body;
42
+ const pre = document.createElement('pre');
43
+ pre.style.cssText = 'position:fixed;bottom:60px;left:0;right:0;background:#b00;color:#fff;padding:12px;white-space:pre-wrap;font-size:12px;z-index:9999;';
44
+ pre.textContent = '[window error] ' + (event.message || '') + '\n' + ((event.error && event.error.stack) || (event.filename + ':' + event.lineno + ':' + event.colno));
45
+ target.appendChild(pre);
46
+ } catch (_) {}
47
+ });
48
+ window.addEventListener('unhandledrejection', (event) => {
49
+ console.error('[unhandled rejection]', event.reason);
50
+ try {
51
+ const target = document.querySelector('#app') || document.body;
52
+ const pre = document.createElement('pre');
53
+ pre.style.cssText = 'position:fixed;bottom:120px;left:0;right:0;background:#b00;color:#fff;padding:12px;white-space:pre-wrap;font-size:12px;z-index:9999;';
54
+ pre.textContent = '[unhandled rejection] ' + (event.reason && event.reason.message ? event.reason.message : String(event.reason)) + '\n' + ((event.reason && event.reason.stack) || '');
55
+ target.appendChild(pre);
56
+ } catch (_) {}
57
+ });
29
58
 
30
59
  const appOptions = {
31
60
  data() {
@@ -96,6 +125,29 @@ document.addEventListener('DOMContentLoaded', () => {
96
125
  showCodexBridgePoolModal: false,
97
126
  showClaudeBridgePoolModal: false,
98
127
  showWebhookModal: false,
128
+ piProviders: {},
129
+ piProviderIds: [],
130
+ editingPiProvider: null,
131
+ addingPiProviderId: '',
132
+ addingPiProviderName: '',
133
+ addingPiProviderBaseUrl: '',
134
+ addingPiProviderApi: 'openai-completions',
135
+ addingPiProviderApiKey: '',
136
+ addingPiProviderModel: '',
137
+ addingPiRemoteModels: [],
138
+ addingPiRemoteLoading: false,
139
+ addingPiRemoteError: '',
140
+ addingPiRemoteDebounce: null,
141
+ showAddPiProviderModal: false,
142
+ piProviderLoading: false,
143
+ piSaving: false,
144
+ piRemoteModels: [],
145
+ piRemoteModelsLoading: false,
146
+ piRemoteModelError: '',
147
+ piModelSearch: '',
148
+ piActiveProvider: '',
149
+ piActiveModel: '',
150
+ piShowKey: false,
99
151
  // Plugins
100
152
  pluginsActiveId: 'prompt-templates',
101
153
  pluginsLoading: false,
@@ -137,7 +189,7 @@ document.addEventListener('DOMContentLoaded', () => {
137
189
  configTemplateDiffFingerprint: '',
138
190
  _configTemplateDiffPreviewRequestToken: null,
139
191
  configTemplateDiffConfirmEnabled: true,
140
- configModeVisibility: { codex: true, claude: true, openclaw: true, opencode: true, kilocode: true },
192
+ configModeVisibility: { codex: true, claude: true, openclaw: true, opencode: true, kilocode: true, pi: true },
141
193
  codexApplying: false,
142
194
  _pendingCodexApplyOptions: null,
143
195
  agentsContent: '',
@@ -433,8 +485,8 @@ document.addEventListener('DOMContentLoaded', () => {
433
485
  providerCacheError: '',
434
486
  providerCacheRequestSeq: 0,
435
487
  settingsTab: 'general',
436
- toolConfigPermissions: { codex: false, claude: false, opencode: false, kilocode: false, openclaw: false },
437
- toolConfigPermissionSaving: { codex: false, claude: false, opencode: false, kilocode: false, openclaw: false },
488
+ toolConfigPermissions: { codex: false, claude: false, opencode: false, kilocode: false, openclaw: false, pi: false },
489
+ toolConfigPermissionSaving: { codex: false, claude: false, opencode: false, kilocode: false, openclaw: false, pi: false },
438
490
  sessionTrashEnabled: true,
439
491
  sessionTrashItems: [],
440
492
  sessionTrashVisibleCount: SESSION_TRASH_PAGE_SIZE,
@@ -535,7 +587,11 @@ document.addEventListener('DOMContentLoaded', () => {
535
587
  return true;
536
588
  }
537
589
  })();
538
- void this.loadWebUiPreferences({ applyNavigation: applyPreferenceNavigation });
590
+ this.loadWebUiPreferences({ applyNavigation: applyPreferenceNavigation }).then(() => {
591
+ if (this.mainTab === 'prompts' && typeof this.loadPromptsTabContent === 'function') {
592
+ this.loadPromptsTabContent();
593
+ }
594
+ });
539
595
  }
540
596
  if (typeof this.t === 'function') {
541
597
  this.confirmDialogConfirmText = this.t('confirm.ok');
@@ -734,14 +790,7 @@ document.addEventListener('DOMContentLoaded', () => {
734
790
  return;
735
791
  }
736
792
  if (newTab === 'prompts') {
737
- if (this.promptsSubTab === 'claude-project' && !this.projectPathOptions.length && !this.projectPathOptionsLoading && typeof this.loadProjectPathOptions === 'function') {
738
- this.loadProjectPathOptions();
739
- }
740
- if (this.promptsSubTab === 'system') {
741
- if (typeof this.loadSystemPrompt === 'function') this.loadSystemPrompt();
742
- } else if (typeof this.loadPromptsContent === 'function') {
743
- this.loadPromptsContent();
744
- }
793
+ if (typeof this.loadPromptsTabContent === 'function') this.loadPromptsTabContent();
745
794
  }
746
795
  },
747
796
  promptsSubTab(newVal) {
@@ -753,11 +802,7 @@ document.addEventListener('DOMContentLoaded', () => {
753
802
  return;
754
803
  }
755
804
  if (this.mainTab === 'prompts') {
756
- if (this.promptsSubTab === 'system') {
757
- if (typeof this.loadSystemPrompt === 'function') this.loadSystemPrompt();
758
- } else if (typeof this.loadPromptsContent === 'function') {
759
- this.loadPromptsContent();
760
- }
805
+ if (typeof this.loadPromptsTabContent === 'function') this.loadPromptsTabContent();
761
806
  }
762
807
  },
763
808
  projectClaudeMdPath(newPath) {
@@ -777,8 +822,19 @@ document.addEventListener('DOMContentLoaded', () => {
777
822
  }
778
823
  },
779
824
 
825
+ watch: {
826
+ configMode(newMode) {
827
+ if (newMode === 'pi' && typeof this.loadPiSources === 'function') {
828
+ this.loadPiSources();
829
+ }
830
+ }
831
+ },
832
+
780
833
  computed: createAppComputed(),
781
- methods: createAppMethods()
834
+ methods: {
835
+ ...createAppMethods(),
836
+ ...(typeof createPiConfigMethods === 'function' ? createPiConfigMethods({ api }) : {})
837
+ }
782
838
  };
783
839
 
784
840
  if (typeof window.__CODEXMATE_WEB_UI_RENDER__ === 'function') {
@@ -786,6 +842,19 @@ document.addEventListener('DOMContentLoaded', () => {
786
842
  }
787
843
 
788
844
  const app = createApp(appOptions);
845
+ app.config.errorHandler = (err, vm, info) => {
846
+ console.error('[Vue error handler]', err, info);
847
+ if (err && err.stack) console.error(err.stack);
848
+ showFatalErrorOverlay('Vue error', err && err.message ? err.message : String(err), err && err.stack ? err.stack : '', info || '');
849
+ };
789
850
 
790
- app.mount('#app');
851
+ try {
852
+ app.mount('#app');
853
+ } catch (error) {
854
+ console.error('Failed to mount Web UI:', error);
855
+ const fallback = document.querySelector('#app');
856
+ if (fallback) {
857
+ fallback.innerHTML = '<pre style="color:red;white-space:pre-wrap;">Failed to mount Web UI\n' + (error && error.stack ? error.stack : String(error)) + '</pre>';
858
+ }
859
+ }
791
860
  });
package/web-ui/index.html CHANGED
@@ -18,6 +18,7 @@
18
18
  <!-- @include ./partials/index/panel-config-openclaw.html -->
19
19
  <!-- @include ./partials/index/panel-config-opencode.html -->
20
20
  <!-- @include ./partials/index/panel-config-kilocode.html -->
21
+ <!-- @include ./partials/index/panel-config-pi.html -->
21
22
  <!-- @include ./partials/index/panel-sessions.html -->
22
23
  <!-- @include ./partials/index/panel-usage.html -->
23
24
  <!-- @include ./partials/index/panel-docs.html -->
@@ -34,6 +35,6 @@
34
35
  <!-- @include ./partials/index/modal-skills.html -->
35
36
  <!-- @include ./partials/index/modal-health-check.html -->
36
37
  <!-- @include ./partials/index/modal-confirm-toast.html -->
37
- <script type="module" src="/web-ui/app.js"></script>
38
+ <script type="module" src="/web-ui/app.js?cb=3"></script>
38
39
  </body>
39
40
  </html>
@@ -980,17 +980,28 @@ export function createAgentsMethods(options = {}) {
980
980
  this.loadProjectPathOptions();
981
981
  }
982
982
  if (this.promptsSubTab === normalized) {
983
- if (normalized === 'system') this.loadSystemPrompt();
984
- else this.loadPromptsContent();
983
+ if (typeof this.loadPromptsTabContent === 'function') this.loadPromptsTabContent();
985
984
  return;
986
985
  }
986
+ this.__skipNextPromptsSubTabLoad = true;
987
987
  this.promptsSubTab = normalized;
988
- if (normalized === 'system') this.loadSystemPrompt();
988
+ if (typeof this.loadPromptsTabContent === 'function') this.loadPromptsTabContent();
989
989
  this.$nextTick(() => {
990
990
  document.querySelector('.main-panel')?.scrollTo({ top: 0, left: 0, behavior: 'auto' });
991
991
  });
992
992
  },
993
993
 
994
+ loadPromptsTabContent() {
995
+ if (this.promptsSubTab === 'system') {
996
+ if (typeof this.loadSystemPrompt === 'function') this.loadSystemPrompt();
997
+ } else {
998
+ if (this.promptsSubTab === 'claude-project' && !this.projectPathOptions.length && !this.projectPathOptionsLoading && typeof this.loadProjectPathOptions === 'function') {
999
+ this.loadProjectPathOptions();
1000
+ }
1001
+ if (typeof this.loadPromptsContent === 'function') this.loadPromptsContent();
1002
+ }
1003
+ },
1004
+
994
1005
  async loadPromptsContent() {
995
1006
  const requestToken = issueLatestRequestToken(this, '_agentsOpenRequestToken');
996
1007
  this.agentsLoading = true;
@@ -22,6 +22,7 @@ import { createOpenclawEditingMethods } from './app.methods.openclaw-editing.mjs
22
22
  import { createOpenclawPersistMethods } from './app.methods.openclaw-persist.mjs';
23
23
  import { createOpencodeConfigMethods } from './app.methods.opencode-config.mjs';
24
24
  import { createKilocodeConfigMethods } from './app.methods.kilocode-config.mjs';
25
+ import { createPiConfigMethods } from './app.methods.pi-config.mjs';
25
26
  import { createProvidersMethods } from './app.methods.providers.mjs';
26
27
  import { createProviderCacheMethods } from './app.methods.provider-cache.mjs';
27
28
  import { createRuntimeMethods } from './app.methods.runtime.mjs';
@@ -102,6 +103,7 @@ export function createAppMethods() {
102
103
  modelCatalog: OPENCODE_MODEL_CATALOG
103
104
  }),
104
105
  ...createKilocodeConfigMethods({ api }),
106
+ ...createPiConfigMethods({ api }),
105
107
  ...createInstallMethods({ api }),
106
108
  ...createRuntimeMethods({ api })
107
109
  };
@@ -147,7 +147,7 @@
147
147
  }
148
148
  const resolvedMode = configModeSet.has(normalizedMode) ? normalizedMode : 'codex';
149
149
  if (typeof this.isConfigModeVisible === 'function' && !this.isConfigModeVisible(resolvedMode)) {
150
- const fallback = ['codex', 'claude', 'openclaw', 'opencode', 'kilocode'].find(m => this.isConfigModeVisible(m)) || 'codex';
150
+ const fallback = ['codex', 'claude', 'openclaw', 'opencode', 'kilocode', 'pi'].find(m => this.isConfigModeVisible(m)) || 'codex';
151
151
  this.configMode = configModeSet.has(fallback) ? fallback : 'codex';
152
152
  } else {
153
153
  this.configMode = resolvedMode;