remcodex 0.1.0-beta.5 → 0.1.0-beta.7
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 +203 -215
- package/dist/server/src/app.js +1 -3
- package/dist/server/src/cli.js +1 -1
- package/dist/server/src/controllers/session.controller.js +12 -0
- package/dist/server/src/services/session-manager.js +53 -0
- package/docs/assets/approval-flow.png +0 -0
- package/docs/assets/hero-desktop.png +0 -0
- package/docs/assets/imported-session.png +0 -0
- package/docs/assets/mobile-session.png +0 -0
- package/package.json +4 -3
- package/web/api.js +7 -0
- package/web/app.js +59 -10
- package/web/i18n/locales/en.js +1 -0
- package/web/i18n/locales/zh-CN.js +1 -0
- package/web/styles.css +22 -0
- package/LICENSE +0 -21
package/README.md
CHANGED
|
@@ -1,103 +1,45 @@
|
|
|
1
|
-
#
|
|
1
|
+
# RemCodex
|
|
2
2
|
|
|
3
|
-
> Control
|
|
4
|
-
> Built for Codex. Ready for more.
|
|
3
|
+
> Control Codex from anywhere. Even on your phone.
|
|
5
4
|
|
|
6
|
-
|
|
7
|
-
Watch, approve, and control it from any device.
|
|
5
|
+
RemCodex is a local-first web UI for running, reviewing, approving, and resuming Codex sessions from your browser.
|
|
8
6
|
|
|
9
|
-
|
|
10
|
-
> A real-time control layer for AI execution.
|
|
11
|
-
> Turns AI execution into a controllable system.
|
|
7
|
+
It is built for the real workflow: long-running sessions, mobile check-ins, approval prompts, imported rollout history, and timeline-style execution flow.
|
|
12
8
|
|
|
13
|
-
|
|
9
|
+
- Watch live Codex runs without staying in the terminal
|
|
10
|
+
- Approve sensitive actions from a cleaner UI
|
|
11
|
+
- Pick up the same session again after refresh, sleep, or reconnect
|
|
14
12
|
|
|
15
|
-
|
|
13
|
+
## Status
|
|
16
14
|
|
|
17
|
-
|
|
15
|
+
This project is currently a **beta / developer preview**.
|
|
18
16
|
|
|
19
|
-
|
|
17
|
+
It is already usable for local and internal workflows, but it is not yet packaged as a one-click desktop app.
|
|
20
18
|
|
|
21
|
-
|
|
22
|
-
visible, interruptible, and continuous.
|
|
19
|
+
## Why People Use It
|
|
23
20
|
|
|
24
|
-
|
|
25
|
-
- ✅ Approve or reject actions before execution
|
|
26
|
-
- ⏹ Interrupt or stop at any moment
|
|
27
|
-
- 📱 Access your session from any device
|
|
28
|
-
- 🔄 Sessions don’t break — they resume
|
|
21
|
+
Codex is powerful in the terminal, but many real workflows need a better control surface:
|
|
29
22
|
|
|
30
|
-
|
|
23
|
+
- checking progress from your phone
|
|
24
|
+
- watching a long run without babysitting a terminal window
|
|
25
|
+
- approving writes from a cleaner interface
|
|
26
|
+
- reopening a session after refresh, sleep, or reconnect
|
|
27
|
+
- reviewing imported rollout history next to native sessions
|
|
31
28
|
|
|
32
|
-
|
|
29
|
+
RemCodex turns Codex's event stream into a browser-based workspace that is easier to follow, easier to resume, and easier to operate.
|
|
33
30
|
|
|
34
|
-
##
|
|
31
|
+
## What It Does
|
|
35
32
|
|
|
36
|
-
|
|
33
|
+
- Run Codex sessions from a browser
|
|
34
|
+
- View sessions in a single-page workspace with a sidebar and execution timeline
|
|
35
|
+
- Follow streaming assistant output, commands, patches, and approvals
|
|
36
|
+
- Approve or reject file-system actions from the UI
|
|
37
|
+
- Import existing Codex rollout sessions from `~/.codex/sessions/...`
|
|
38
|
+
- Keep imported sessions in sync while they are still active
|
|
39
|
+
- Resume stale sessions after the page comes back from background
|
|
40
|
+
- Work well on desktop and on mobile
|
|
37
41
|
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
On your phone:
|
|
41
|
-
|
|
42
|
-
- you see progress in real time
|
|
43
|
-
- you receive an approval request
|
|
44
|
-
- you approve it
|
|
45
|
-
|
|
46
|
-
The session continues instantly.
|
|
47
|
-
|
|
48
|
-
> Everything else can disconnect — your session won’t.
|
|
49
|
-
|
|
50
|
-
---
|
|
51
|
-
|
|
52
|
-
## 🔥 Why RemCodex exists
|
|
53
|
-
|
|
54
|
-
AI coding agents are powerful.
|
|
55
|
-
But today, they run like a black box.
|
|
56
|
-
|
|
57
|
-
You either:
|
|
58
|
-
|
|
59
|
-
- trust everything blindly
|
|
60
|
-
- or sit in front of your terminal watching it
|
|
61
|
-
|
|
62
|
-
RemCodex fixes that.
|
|
63
|
-
|
|
64
|
-
> AI is no longer a black box.
|
|
65
|
-
|
|
66
|
-
---
|
|
67
|
-
|
|
68
|
-
## ⚡ What it does
|
|
69
|
-
|
|
70
|
-
- Real-time execution timeline (messages, commands, approvals)
|
|
71
|
-
- Human-in-the-loop command approval
|
|
72
|
-
- Multi-device access to the same live session
|
|
73
|
-
- Resume after refresh, sleep, or reconnect
|
|
74
|
-
- Browser-based UI — **no extra client required**
|
|
75
|
-
- Works with Codex CLI
|
|
76
|
-
|
|
77
|
-
> No extra client install. Just open a browser.
|
|
78
|
-
> Your code never leaves your machine.
|
|
79
|
-
|
|
80
|
-
---
|
|
81
|
-
|
|
82
|
-
## 🚀 Quick start
|
|
83
|
-
|
|
84
|
-
```bash
|
|
85
|
-
npx remcodex
|
|
86
|
-
```
|
|
87
|
-
|
|
88
|
-
Then open:
|
|
89
|
-
|
|
90
|
-
http://127.0.0.1:18840
|
|
91
|
-
|
|
92
|
-
Access from another device:
|
|
93
|
-
|
|
94
|
-
http://<your-ip>:18840
|
|
95
|
-
|
|
96
|
-
> Runs entirely on your local machine. No cloud, no data upload.
|
|
97
|
-
|
|
98
|
-
---
|
|
99
|
-
|
|
100
|
-
## 🖥 Screenshots
|
|
42
|
+
## Screenshots
|
|
101
43
|
|
|
102
44
|

|
|
103
45
|
|
|
@@ -113,203 +55,249 @@ http://<your-ip>:18840
|
|
|
113
55
|
|
|
114
56
|

|
|
115
57
|
|
|
116
|
-
> Bring imported Codex rollouts into the same workspace.
|
|
117
|
-
|
|
118
|
-
---
|
|
119
|
-
|
|
120
|
-
## 🧠 What it actually is
|
|
58
|
+
> Bring imported Codex rollouts into the same workspace and keep them easy to review.
|
|
121
59
|
|
|
122
|
-
|
|
60
|
+
## Who It Is For
|
|
123
61
|
|
|
124
|
-
|
|
62
|
+
- developers who already use Codex locally
|
|
63
|
+
- people who want a browser-based control surface instead of raw terminal watching
|
|
64
|
+
- teams who want to review or monitor runs from another device on the same network
|
|
65
|
+
- anyone who wants approvals, timeline view, and imported rollout history in one place
|
|
125
66
|
|
|
126
|
-
|
|
127
|
-
- mobile check-ins
|
|
128
|
-
- approval prompts
|
|
129
|
-
- imported rollout history
|
|
130
|
-
- timeline-style execution flow
|
|
67
|
+
## Screens It Aims To Replace
|
|
131
68
|
|
|
132
|
-
|
|
69
|
+
- terminal-only session watching
|
|
70
|
+
- ad-hoc mobile remote desktop checks
|
|
71
|
+
- raw log scrolling for approvals and command progress
|
|
72
|
+
- fragmented session history between local and imported rollouts
|
|
133
73
|
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
## 🧩 Current product shape
|
|
74
|
+
## Current Product Shape
|
|
137
75
|
|
|
138
76
|
- Single-page workspace UI
|
|
139
77
|
- Left sidebar for session navigation
|
|
140
|
-
- Right-side execution
|
|
141
|
-
- Fixed
|
|
78
|
+
- Right-side timeline / execution flow for the active session
|
|
79
|
+
- Fixed composer at the bottom
|
|
142
80
|
- Semantic timeline rendering for:
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
---
|
|
152
|
-
|
|
153
|
-
## ⚙️ Key behaviors
|
|
154
|
-
|
|
155
|
-
### Approvals
|
|
156
|
-
|
|
157
|
-
- Writes inside working area → auto allowed
|
|
158
|
-
- Writes outside → require approval
|
|
159
|
-
- `Allow once` / `Allow for this turn` supported
|
|
160
|
-
- Approval history stays visible in timeline
|
|
81
|
+
- user messages
|
|
82
|
+
- assistant commentary / final messages
|
|
83
|
+
- thinking
|
|
84
|
+
- commands
|
|
85
|
+
- patches
|
|
86
|
+
- approvals
|
|
87
|
+
- system events
|
|
161
88
|
|
|
162
|
-
|
|
89
|
+
## Tech Stack
|
|
163
90
|
|
|
164
|
-
|
|
91
|
+
- Backend: Node.js + TypeScript + Express + WebSocket
|
|
92
|
+
- Database: SQLite via `better-sqlite3`
|
|
93
|
+
- Terminal/runtime integration: `node-pty` + Codex app-server
|
|
94
|
+
- Frontend: zero-build static web app (`web/`)
|
|
95
|
+
- Markdown rendering: `markdown-it`
|
|
165
96
|
|
|
166
|
-
|
|
167
|
-
- Commands grouped into readable activity blocks
|
|
168
|
-
- Running / failed states clearly visible
|
|
169
|
-
- Smooth streaming + recovery after refresh
|
|
97
|
+
## Requirements
|
|
170
98
|
|
|
171
|
-
|
|
99
|
+
Before running this project, you should have:
|
|
172
100
|
|
|
173
|
-
|
|
101
|
+
- Node.js installed
|
|
102
|
+
- Codex CLI installed and already working locally
|
|
103
|
+
- A machine where this app can access your local Codex data and working directories
|
|
174
104
|
|
|
175
|
-
|
|
176
|
-
- Keep syncing if still active
|
|
177
|
-
- Unified view with native sessions
|
|
105
|
+
This project is currently developed primarily around a local macOS workflow.
|
|
178
106
|
|
|
179
|
-
|
|
107
|
+
## Quick Start
|
|
180
108
|
|
|
181
|
-
|
|
109
|
+
For the current developer preview, the recommended local install path is:
|
|
182
110
|
|
|
183
|
-
```
|
|
184
|
-
|
|
111
|
+
```bash
|
|
112
|
+
npm install
|
|
113
|
+
npm run build
|
|
114
|
+
npm link
|
|
115
|
+
remcodex start
|
|
185
116
|
```
|
|
186
117
|
|
|
187
|
-
|
|
118
|
+
Then open:
|
|
188
119
|
|
|
189
|
-
|
|
120
|
+
```text
|
|
121
|
+
http://127.0.0.1:3000
|
|
122
|
+
```
|
|
190
123
|
|
|
191
|
-
|
|
192
|
-
- Codex CLI (already working locally)
|
|
124
|
+
If you want to make it reachable from your phone, expose the local machine on your LAN and open the same URL with your host IP.
|
|
193
125
|
|
|
194
|
-
|
|
126
|
+
## Local CLI
|
|
195
127
|
|
|
196
|
-
|
|
128
|
+
RemCodex already ships with a local CLI entrypoint, even though the npm package is not published yet.
|
|
197
129
|
|
|
198
|
-
|
|
130
|
+
If you do not want to run `npm link`, you can call the built CLI directly:
|
|
199
131
|
|
|
200
132
|
```bash
|
|
201
|
-
|
|
133
|
+
node dist/server/src/cli.js start --no-open
|
|
202
134
|
```
|
|
203
135
|
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
## 📦 Install FAQ
|
|
207
|
-
|
|
208
|
-
### Why does `npx remcodex` hang on Linux?
|
|
209
|
-
|
|
210
|
-
First install may compile native deps:
|
|
211
|
-
|
|
212
|
-
- `better-sqlite3`
|
|
213
|
-
- `node-pty`
|
|
136
|
+
Useful commands:
|
|
214
137
|
|
|
215
|
-
|
|
138
|
+
```bash
|
|
139
|
+
node dist/server/src/cli.js doctor
|
|
140
|
+
node dist/server/src/cli.js start --no-open
|
|
141
|
+
node dist/server/src/cli.js version
|
|
142
|
+
```
|
|
216
143
|
|
|
217
|
-
|
|
218
|
-
- `make`
|
|
219
|
-
- `g++`
|
|
144
|
+
Use a specific database:
|
|
220
145
|
|
|
221
|
-
|
|
146
|
+
```bash
|
|
147
|
+
node dist/server/src/cli.js start --db ~/.remcodex/remcodex-demo.db --no-open
|
|
148
|
+
node dist/server/src/cli.js doctor --db ~/.remcodex/remcodex-demo.db
|
|
149
|
+
```
|
|
222
150
|
|
|
223
|
-
|
|
151
|
+
Planned install target after the npm package is published:
|
|
224
152
|
|
|
225
153
|
```bash
|
|
226
|
-
|
|
227
|
-
remcodex doctor
|
|
228
|
-
remcodex start
|
|
154
|
+
npx remcodex
|
|
229
155
|
```
|
|
230
156
|
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
### Headless mode
|
|
157
|
+
## Development
|
|
234
158
|
|
|
235
159
|
```bash
|
|
236
|
-
|
|
160
|
+
npm install
|
|
161
|
+
npm run dev
|
|
237
162
|
```
|
|
238
163
|
|
|
239
|
-
|
|
164
|
+
## How It Works
|
|
165
|
+
|
|
166
|
+
The app uses `codex app-server` as the primary runtime path.
|
|
240
167
|
|
|
241
|
-
|
|
168
|
+
At a high level:
|
|
242
169
|
|
|
243
|
-
1. Codex emits events
|
|
244
|
-
2.
|
|
245
|
-
3.
|
|
246
|
-
4. Live updates
|
|
170
|
+
1. Codex emits semantic events
|
|
171
|
+
2. The backend stores them in SQLite
|
|
172
|
+
3. The frontend reads an aggregated timeline view for initial load
|
|
173
|
+
4. Live updates continue over WebSocket with catch-up after refresh
|
|
247
174
|
|
|
248
|
-
|
|
175
|
+
This gives the UI:
|
|
249
176
|
|
|
177
|
+
- smooth streaming
|
|
250
178
|
- recoverable sessions
|
|
251
|
-
-
|
|
252
|
-
- consistent execution
|
|
179
|
+
- imported rollout support
|
|
180
|
+
- a consistent execution timeline instead of raw terminal logs
|
|
253
181
|
|
|
254
|
-
|
|
182
|
+
## Key Behaviors
|
|
255
183
|
|
|
256
|
-
|
|
184
|
+
### Approvals
|
|
257
185
|
|
|
258
|
-
-
|
|
259
|
-
-
|
|
260
|
-
-
|
|
186
|
+
- Writes inside the working area usually pass directly
|
|
187
|
+
- Writes outside the working area trigger approval
|
|
188
|
+
- `Allow once` approves only the current request
|
|
189
|
+
- `Allow for this turn` expands writable roots for the active turn
|
|
190
|
+
- Historical approval records stay visible in the timeline
|
|
191
|
+
- Only live approvals stay actionable in the bottom approval bar
|
|
261
192
|
|
|
262
|
-
|
|
193
|
+
### Imported Codex Sessions
|
|
263
194
|
|
|
264
|
-
|
|
195
|
+
- Existing Codex rollouts can be imported from local session history
|
|
196
|
+
- Imported sessions keep their own source metadata
|
|
197
|
+
- Imported sessions can continue syncing after you open them
|
|
198
|
+
- Native sessions are excluded from the import picker
|
|
265
199
|
|
|
266
|
-
|
|
200
|
+
### Timeline and Execution Flow
|
|
267
201
|
|
|
268
|
-
-
|
|
269
|
-
-
|
|
202
|
+
- The UI renders semantic timeline items, not raw logs
|
|
203
|
+
- Commands and patches can be grouped into lighter activity summaries
|
|
204
|
+
- Running and failed commands remain visually important
|
|
205
|
+
- The final thinking placeholder appears only at the end of the active flow
|
|
270
206
|
|
|
271
|
-
|
|
207
|
+
## Configuration
|
|
272
208
|
|
|
273
|
-
|
|
274
|
-
- safer execution
|
|
209
|
+
Supported environment variables:
|
|
275
210
|
|
|
276
|
-
|
|
211
|
+
- `PORT`
|
|
212
|
+
- `DATABASE_PATH`
|
|
213
|
+
- `PROJECT_ROOTS`
|
|
214
|
+
- `CODEX_COMMAND`
|
|
215
|
+
- `CODEX_MODE`
|
|
277
216
|
|
|
278
|
-
|
|
279
|
-
- stable long runs
|
|
217
|
+
For launch screenshots or demo data, you can rebuild a clean demo database with:
|
|
280
218
|
|
|
281
|
-
|
|
219
|
+
```bash
|
|
220
|
+
DATABASE_PATH="$HOME/.remcodex/remcodex-demo.db" ~/.nvm/versions/node/v20.19.5/bin/node scripts/seed-launch-demo-data.js --clean
|
|
221
|
+
```
|
|
222
|
+
- `REMOTE_HOSTS`
|
|
223
|
+
- `ACTIVE_REMOTE_HOST`
|
|
224
|
+
|
|
225
|
+
Notes:
|
|
226
|
+
|
|
227
|
+
- The default runtime mode is `app-server`
|
|
228
|
+
- `exec-json` is kept only as a fallback compatibility path
|
|
229
|
+
- If `PROJECT_ROOTS` is not set, the app falls back to a broad local browsing root
|
|
230
|
+
|
|
231
|
+
## Project Structure
|
|
232
|
+
|
|
233
|
+
```text
|
|
234
|
+
server/
|
|
235
|
+
src/
|
|
236
|
+
app.ts
|
|
237
|
+
controllers/
|
|
238
|
+
db/
|
|
239
|
+
gateways/
|
|
240
|
+
services/
|
|
241
|
+
types/
|
|
242
|
+
utils/
|
|
243
|
+
web/
|
|
244
|
+
index.html
|
|
245
|
+
styles.css
|
|
246
|
+
api.js
|
|
247
|
+
session-ws.js
|
|
248
|
+
app.js
|
|
249
|
+
scripts/
|
|
250
|
+
fix-node-pty-helper.js
|
|
251
|
+
import-codex-rollout.js
|
|
252
|
+
reset-semantic-demo-data.js
|
|
253
|
+
```
|
|
282
254
|
|
|
283
|
-
|
|
255
|
+
## Main Endpoints
|
|
284
256
|
|
|
285
|
-
|
|
257
|
+
- `GET /health`
|
|
258
|
+
- `GET /api/codex/mode`
|
|
259
|
+
- `GET /api/codex/status`
|
|
260
|
+
- `GET /api/codex/quota`
|
|
261
|
+
- `GET /api/sessions`
|
|
262
|
+
- `GET /api/sessions/:sessionId`
|
|
263
|
+
- `GET /api/sessions/:sessionId/timeline`
|
|
264
|
+
- `GET /api/sessions/:sessionId/events`
|
|
265
|
+
- `POST /api/sessions`
|
|
266
|
+
- `POST /api/sessions/:sessionId/messages`
|
|
267
|
+
- `POST /api/sessions/:sessionId/stop`
|
|
268
|
+
- `POST /api/sessions/:sessionId/approvals/:requestId`
|
|
269
|
+
- `WS /ws/sessions/:sessionId`
|
|
286
270
|
|
|
287
|
-
|
|
288
|
-
- optional sharing
|
|
271
|
+
## What Is Not Finished Yet
|
|
289
272
|
|
|
290
|
-
|
|
273
|
+
This is the honest list:
|
|
291
274
|
|
|
292
|
-
|
|
275
|
+
- no polished installer yet
|
|
276
|
+
- no desktop packaging yet
|
|
277
|
+
- no full automated test suite yet
|
|
278
|
+
- no production-grade auth / multi-user hardening yet
|
|
279
|
+
- no release pipeline yet
|
|
293
280
|
|
|
294
|
-
|
|
295
|
-
- people tired of terminal-only workflows
|
|
296
|
-
- anyone who wants **control, not just output**
|
|
297
|
-
- multi-device workflows
|
|
281
|
+
If you are comfortable cloning a repo and running a local Node app, you can use it today.
|
|
298
282
|
|
|
299
|
-
|
|
283
|
+
## Roadmap
|
|
300
284
|
|
|
301
|
-
|
|
285
|
+
Near-term:
|
|
302
286
|
|
|
303
|
-
-
|
|
304
|
-
-
|
|
305
|
-
-
|
|
306
|
-
-
|
|
287
|
+
- improve onboarding and installation
|
|
288
|
+
- ship a cleaner public README and screenshots
|
|
289
|
+
- add stronger regression coverage
|
|
290
|
+
- harden long-running session recovery
|
|
291
|
+
- continue refining the execution timeline UI
|
|
292
|
+
|
|
293
|
+
Later:
|
|
307
294
|
|
|
308
|
-
|
|
309
|
-
|
|
295
|
+
- package for easier local install
|
|
296
|
+
- optional sync / multi-device helpers
|
|
297
|
+
- stronger sharing, auditing, and team workflows
|
|
310
298
|
|
|
311
|
-
|
|
299
|
+
## License
|
|
312
300
|
|
|
313
|
-
|
|
301
|
+
No license has been added yet.
|
|
314
302
|
|
|
315
|
-
|
|
303
|
+
Until a license is added, assume this repository is **source-available for review only**, not open source for reuse.
|
package/dist/server/src/app.js
CHANGED
|
@@ -3,7 +3,6 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
3
3
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
4
|
};
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
-
exports.DEFAULT_PORT = void 0;
|
|
7
6
|
exports.resolvePackageRoot = resolvePackageRoot;
|
|
8
7
|
exports.resolveDefaultDatabasePath = resolveDefaultDatabasePath;
|
|
9
8
|
exports.startRemCodexServer = startRemCodexServer;
|
|
@@ -47,10 +46,9 @@ function resolvePackageRoot(startDir = __dirname) {
|
|
|
47
46
|
function resolveDefaultDatabasePath() {
|
|
48
47
|
return node_path_1.default.join((0, node_os_1.homedir)(), ".remcodex", "remcodex.db");
|
|
49
48
|
}
|
|
50
|
-
exports.DEFAULT_PORT = 18840;
|
|
51
49
|
function buildRemCodexServer(options = {}) {
|
|
52
50
|
const repoRoot = options.repoRoot ? node_path_1.default.resolve(options.repoRoot) : resolvePackageRoot();
|
|
53
|
-
const port = options.port ?? Number.parseInt(process.env.PORT ??
|
|
51
|
+
const port = options.port ?? Number.parseInt(process.env.PORT ?? "3000", 10);
|
|
54
52
|
const databasePath = options.databasePath ??
|
|
55
53
|
process.env.DATABASE_PATH ??
|
|
56
54
|
resolveDefaultDatabasePath();
|
package/dist/server/src/cli.js
CHANGED
|
@@ -169,7 +169,7 @@ async function runStart(flags) {
|
|
|
169
169
|
printError("Install Codex first, or set CODEX_COMMAND to the correct executable.");
|
|
170
170
|
return 1;
|
|
171
171
|
}
|
|
172
|
-
const preferredPort = flags.port ?? Number.parseInt(process.env.PORT ??
|
|
172
|
+
const preferredPort = flags.port ?? Number.parseInt(process.env.PORT ?? "3000", 10);
|
|
173
173
|
const codexMode = process.env.CODEX_MODE === "exec-json" ? "exec-json" : "app-server";
|
|
174
174
|
let started = null;
|
|
175
175
|
let activePort = preferredPort;
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.createSessionRouter = createSessionRouter;
|
|
4
4
|
const express_1 = require("express");
|
|
5
|
+
const codex_launch_1 = require("../utils/codex-launch");
|
|
5
6
|
function createSessionRouter(sessionManager, eventStore, projectManager, codexRolloutSync, sessionTimeline) {
|
|
6
7
|
const router = (0, express_1.Router)();
|
|
7
8
|
router.get("/", (_request, response) => {
|
|
@@ -171,5 +172,16 @@ function createSessionRouter(sessionManager, eventStore, projectManager, codexRo
|
|
|
171
172
|
next(error);
|
|
172
173
|
}
|
|
173
174
|
});
|
|
175
|
+
router.post("/:sessionId/approvals/:requestId/retry", (request, response, next) => {
|
|
176
|
+
try {
|
|
177
|
+
const body = request.body;
|
|
178
|
+
const launch = (0, codex_launch_1.normalizeCodexExecLaunchInput)(body.codex);
|
|
179
|
+
const result = sessionManager.retryApprovalRequest(request.params.sessionId, request.params.requestId, launch);
|
|
180
|
+
response.json(result);
|
|
181
|
+
}
|
|
182
|
+
catch (error) {
|
|
183
|
+
next(error);
|
|
184
|
+
}
|
|
185
|
+
});
|
|
174
186
|
return router;
|
|
175
187
|
}
|
|
@@ -277,6 +277,30 @@ class SessionManager {
|
|
|
277
277
|
seq: event.seq,
|
|
278
278
|
};
|
|
279
279
|
}
|
|
280
|
+
retryApprovalRequest(sessionId, requestId, codexLaunch) {
|
|
281
|
+
const session = this.getSessionOrThrow(sessionId);
|
|
282
|
+
const project = this.options.projectManager.getProject(session.project_id);
|
|
283
|
+
if (!project) {
|
|
284
|
+
throw new errors_1.AppError(404, "Project not found for session.");
|
|
285
|
+
}
|
|
286
|
+
const currentRunner = this.runners.get(sessionId);
|
|
287
|
+
const busyStatuses = ["starting", "running", "stopping"];
|
|
288
|
+
if (currentRunner?.runner.isAlive() && busyStatuses.includes(session.status)) {
|
|
289
|
+
throw new errors_1.AppError(409, "Session already has an active task.");
|
|
290
|
+
}
|
|
291
|
+
const pending = this.pendingApprovals.get(sessionId)?.get(requestId) ??
|
|
292
|
+
this.restorePendingApprovalFromEvents(sessionId, requestId);
|
|
293
|
+
if (!pending) {
|
|
294
|
+
throw new errors_1.AppError(404, "Approval request not found.");
|
|
295
|
+
}
|
|
296
|
+
const turnId = (0, ids_1.createId)("turn");
|
|
297
|
+
const runtimePrompt = normalizeDemoPrompt(project.path, this.buildApprovalRetryRuntimePrompt(pending));
|
|
298
|
+
this.startRunner(sessionId, project.path, runtimePrompt, turnId, this.resolveResumeThreadId(session), codexLaunch);
|
|
299
|
+
return {
|
|
300
|
+
accepted: true,
|
|
301
|
+
turnId,
|
|
302
|
+
};
|
|
303
|
+
}
|
|
280
304
|
stopSession(sessionId) {
|
|
281
305
|
const runtime = this.runners.get(sessionId);
|
|
282
306
|
if (!runtime || !runtime.runner.isAlive()) {
|
|
@@ -1368,6 +1392,35 @@ class SessionManager {
|
|
|
1368
1392
|
}
|
|
1369
1393
|
return null;
|
|
1370
1394
|
}
|
|
1395
|
+
buildApprovalRetryRuntimePrompt(approval) {
|
|
1396
|
+
const commandText = this.extractApprovalCommand(approval.method, approval.params);
|
|
1397
|
+
const reason = typeof approval.params.reason === "string" && approval.params.reason.trim()
|
|
1398
|
+
? approval.params.reason.trim()
|
|
1399
|
+
: "";
|
|
1400
|
+
if (commandText) {
|
|
1401
|
+
return [
|
|
1402
|
+
"Re-run the exact operation that previously requested approval.",
|
|
1403
|
+
"Do not do extra exploration.",
|
|
1404
|
+
"As soon as the approval prompt appears again, stop and wait for the user decision.",
|
|
1405
|
+
"",
|
|
1406
|
+
commandText,
|
|
1407
|
+
].join("\n");
|
|
1408
|
+
}
|
|
1409
|
+
if (reason) {
|
|
1410
|
+
return [
|
|
1411
|
+
"Re-run the exact step that previously requested approval.",
|
|
1412
|
+
"Do not do extra exploration.",
|
|
1413
|
+
"As soon as the approval prompt appears again, stop and wait for the user decision.",
|
|
1414
|
+
"",
|
|
1415
|
+
`Original approval reason: ${reason}`,
|
|
1416
|
+
].join("\n");
|
|
1417
|
+
}
|
|
1418
|
+
return [
|
|
1419
|
+
"Re-run the exact step that previously requested approval.",
|
|
1420
|
+
"Do not do extra exploration.",
|
|
1421
|
+
"As soon as the approval prompt appears again, stop and wait for the user decision.",
|
|
1422
|
+
].join("\n");
|
|
1423
|
+
}
|
|
1371
1424
|
describeApprovalTitle(method) {
|
|
1372
1425
|
if (method === "item/commandExecution/requestApproval" || method === "execCommandApproval") {
|
|
1373
1426
|
return "Command execution requires approval";
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
package/package.json
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "remcodex",
|
|
3
|
-
"version": "0.1.0-beta.
|
|
3
|
+
"version": "0.1.0-beta.7",
|
|
4
4
|
"description": "Control Codex from anywhere. Even on your phone.",
|
|
5
|
-
"license": "
|
|
5
|
+
"license": "UNLICENSED",
|
|
6
6
|
"bin": {
|
|
7
7
|
"remcodex": "dist/server/src/cli.js"
|
|
8
8
|
},
|
|
@@ -10,7 +10,8 @@
|
|
|
10
10
|
"dist",
|
|
11
11
|
"web",
|
|
12
12
|
"scripts/fix-node-pty-helper.js",
|
|
13
|
-
"README.md"
|
|
13
|
+
"README.md",
|
|
14
|
+
"docs/assets"
|
|
14
15
|
],
|
|
15
16
|
"scripts": {
|
|
16
17
|
"postinstall": "node scripts/fix-node-pty-helper.js",
|
package/web/api.js
CHANGED
|
@@ -138,6 +138,13 @@ export function resolveSessionApproval(sessionId, requestId, decision) {
|
|
|
138
138
|
});
|
|
139
139
|
}
|
|
140
140
|
|
|
141
|
+
export function retrySessionApproval(sessionId, requestId, payload = {}) {
|
|
142
|
+
return request(`/api/sessions/${sessionId}/approvals/${encodeURIComponent(requestId)}/retry`, {
|
|
143
|
+
method: "POST",
|
|
144
|
+
body: JSON.stringify(payload),
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
|
|
141
148
|
export function getHealth() {
|
|
142
149
|
return request("/health");
|
|
143
150
|
}
|
package/web/app.js
CHANGED
|
@@ -15,6 +15,7 @@ import {
|
|
|
15
15
|
getSessionTimelineEvents,
|
|
16
16
|
getSessions,
|
|
17
17
|
resolveSessionApproval,
|
|
18
|
+
retrySessionApproval,
|
|
18
19
|
sendMessage,
|
|
19
20
|
stopSession,
|
|
20
21
|
syncImportedSession,
|
|
@@ -613,14 +614,10 @@ function resolveDetailPendingApproval(session, timelineState) {
|
|
|
613
614
|
const sessionPending = session?.pendingApproval || null;
|
|
614
615
|
const liveBusy = session?.liveBusy === true;
|
|
615
616
|
const sessionId = String(session?.sessionId || "").trim();
|
|
616
|
-
|
|
617
|
-
if (!liveBusy) {
|
|
618
|
-
return null;
|
|
619
|
-
}
|
|
617
|
+
const canResolve = liveBusy && sessionPending?.resumable !== false;
|
|
620
618
|
|
|
621
619
|
if (!timelinePending) {
|
|
622
620
|
return sessionPending &&
|
|
623
|
-
sessionPending.resumable !== false &&
|
|
624
621
|
!isApprovalSuppressed(sessionId, sessionPending.requestId, sessionPending.callId)
|
|
625
622
|
? sessionPending
|
|
626
623
|
: null;
|
|
@@ -629,7 +626,7 @@ function resolveDetailPendingApproval(session, timelineState) {
|
|
|
629
626
|
return {
|
|
630
627
|
...timelinePending,
|
|
631
628
|
...(sessionPending && sessionPending.requestId === timelinePending.requestId ? sessionPending : {}),
|
|
632
|
-
resumable:
|
|
629
|
+
resumable: canResolve,
|
|
633
630
|
};
|
|
634
631
|
}
|
|
635
632
|
|
|
@@ -7041,6 +7038,15 @@ function renderPendingApprovalBar(detailState) {
|
|
|
7041
7038
|
const restoreHint = !canResolve
|
|
7042
7039
|
? `<p class="approval-banner-meta approval-banner-meta--warning">${escapeHtml(t("approval.restoreHint"))}</p>`
|
|
7043
7040
|
: "";
|
|
7041
|
+
const actionHtml = canResolve
|
|
7042
|
+
? `
|
|
7043
|
+
<button type="button" class="secondary-button" data-approval-decision="decline">${escapeHtml(t("approval.deny"))}</button>
|
|
7044
|
+
<button type="button" class="secondary-button" data-approval-decision="accept">${escapeHtml(t("approval.allowOnce"))}</button>
|
|
7045
|
+
<button type="button" class="primary-button" data-approval-decision="acceptForSession">${escapeHtml(t("approval.allowForTurn"))}</button>
|
|
7046
|
+
`
|
|
7047
|
+
: `
|
|
7048
|
+
<button type="button" class="primary-button" data-approval-retry="true">${escapeHtml(t("approval.retryAction"))}</button>
|
|
7049
|
+
`;
|
|
7044
7050
|
|
|
7045
7051
|
return `
|
|
7046
7052
|
<section class="approval-banner" data-approval-id="${escapeHtml(approval.requestId)}" data-approval-resumable="${canResolve ? "true" : "false"}">
|
|
@@ -7073,9 +7079,7 @@ function renderPendingApprovalBar(detailState) {
|
|
|
7073
7079
|
${restoreHint}
|
|
7074
7080
|
</div>
|
|
7075
7081
|
<div class="approval-banner-actions">
|
|
7076
|
-
|
|
7077
|
-
<button type="button" class="secondary-button" data-approval-decision="accept" ${canResolve ? "" : "disabled"}>${escapeHtml(t("approval.allowOnce"))}</button>
|
|
7078
|
-
<button type="button" class="primary-button" data-approval-decision="acceptForSession" ${canResolve ? "" : "disabled"}>${escapeHtml(t("approval.allowForTurn"))}</button>
|
|
7082
|
+
${actionHtml}
|
|
7079
7083
|
</div>
|
|
7080
7084
|
</section>
|
|
7081
7085
|
`;
|
|
@@ -7083,7 +7087,52 @@ function renderPendingApprovalBar(detailState) {
|
|
|
7083
7087
|
|
|
7084
7088
|
function bindPendingApprovalControls(sessionId) {
|
|
7085
7089
|
const banner = document.querySelector("#session-approval-slot .approval-banner");
|
|
7086
|
-
if (!banner
|
|
7090
|
+
if (!banner) {
|
|
7091
|
+
return;
|
|
7092
|
+
}
|
|
7093
|
+
|
|
7094
|
+
const retryButton = banner.querySelector("[data-approval-retry]");
|
|
7095
|
+
if (retryButton instanceof HTMLButtonElement) {
|
|
7096
|
+
retryButton.onclick = async () => {
|
|
7097
|
+
const approval = state.detail.pendingApproval;
|
|
7098
|
+
const requestId = banner.getAttribute("data-approval-id");
|
|
7099
|
+
if (!approval || !requestId) {
|
|
7100
|
+
return;
|
|
7101
|
+
}
|
|
7102
|
+
|
|
7103
|
+
const previousPendingApproval = { ...approval };
|
|
7104
|
+
const previousStatus = state.detail.session?.status || "waiting_input";
|
|
7105
|
+
const previousLiveBusy = Boolean(state.detail.session?.liveBusy);
|
|
7106
|
+
retryButton.disabled = true;
|
|
7107
|
+
banner.setAttribute("aria-busy", "true");
|
|
7108
|
+
state.detail.pendingApproval = null;
|
|
7109
|
+
if (state.detail.session?.sessionId === sessionId) {
|
|
7110
|
+
state.detail.session.status = "running";
|
|
7111
|
+
state.detail.session.liveBusy = true;
|
|
7112
|
+
}
|
|
7113
|
+
scheduleSessionDetailRender({ immediate: true });
|
|
7114
|
+
|
|
7115
|
+
try {
|
|
7116
|
+
const codex = buildCodexLaunchPayload(
|
|
7117
|
+
state.detail.codexLaunch,
|
|
7118
|
+
state.detail.codexUiOptions,
|
|
7119
|
+
);
|
|
7120
|
+
const payload = codex ? { codex } : {};
|
|
7121
|
+
await retrySessionApproval(sessionId, requestId, payload);
|
|
7122
|
+
await resumeActiveSessionDetail("approval-retry");
|
|
7123
|
+
} catch (error) {
|
|
7124
|
+
state.detail.pendingApproval = previousPendingApproval;
|
|
7125
|
+
if (state.detail.session?.sessionId === sessionId) {
|
|
7126
|
+
state.detail.session.status = previousStatus;
|
|
7127
|
+
state.detail.session.liveBusy = previousLiveBusy;
|
|
7128
|
+
}
|
|
7129
|
+
scheduleSessionDetailRender({ immediate: true });
|
|
7130
|
+
showToast(messageOf(error));
|
|
7131
|
+
}
|
|
7132
|
+
};
|
|
7133
|
+
}
|
|
7134
|
+
|
|
7135
|
+
if (banner.getAttribute("data-approval-resumable") === "false") {
|
|
7087
7136
|
return;
|
|
7088
7137
|
}
|
|
7089
7138
|
|
package/web/i18n/locales/en.js
CHANGED
|
@@ -198,6 +198,7 @@ export default {
|
|
|
198
198
|
"approval.restore": "Restart required",
|
|
199
199
|
"approval.continueHint": "This step needs your approval before execution can continue.",
|
|
200
200
|
"approval.restoreHint": "This approval request was restored from history and the runtime is no longer active. Send the task again to request approval one more time.",
|
|
201
|
+
"approval.retryAction": "Request again",
|
|
201
202
|
"approval.deny": "Deny",
|
|
202
203
|
"approval.allowOnce": "Allow once",
|
|
203
204
|
"approval.allowForTurn": "Allow for turn",
|
|
@@ -198,6 +198,7 @@ export default {
|
|
|
198
198
|
"approval.restore": "需重新发起",
|
|
199
199
|
"approval.continueHint": "这一步需要你的确认后才能继续执行。",
|
|
200
200
|
"approval.restoreHint": "这条授权请求是从历史事件恢复的,当前运行已经结束。请重新发送这轮任务以再次发起授权。",
|
|
201
|
+
"approval.retryAction": "重新发起授权",
|
|
201
202
|
"approval.deny": "拒绝",
|
|
202
203
|
"approval.allowOnce": "允许一次",
|
|
203
204
|
"approval.allowForTurn": "本轮允许",
|
package/web/styles.css
CHANGED
|
@@ -1394,13 +1394,16 @@ button:hover {
|
|
|
1394
1394
|
|
|
1395
1395
|
.approval-banner {
|
|
1396
1396
|
display: grid;
|
|
1397
|
+
grid-template-rows: minmax(0, 1fr) auto;
|
|
1397
1398
|
gap: 0.6rem;
|
|
1398
1399
|
margin-bottom: 0.7rem;
|
|
1399
1400
|
padding: 0.72rem 0.82rem;
|
|
1401
|
+
max-height: min(46dvh, 24rem);
|
|
1400
1402
|
border-radius: 14px;
|
|
1401
1403
|
border: 1px solid rgba(188, 90, 48, 0.16);
|
|
1402
1404
|
background: rgba(255, 248, 238, 0.96);
|
|
1403
1405
|
box-shadow: 0 6px 16px rgba(169, 77, 38, 0.06);
|
|
1406
|
+
overflow: hidden;
|
|
1404
1407
|
}
|
|
1405
1408
|
|
|
1406
1409
|
.approval-banner[aria-busy="true"] {
|
|
@@ -1411,6 +1414,14 @@ button:hover {
|
|
|
1411
1414
|
.approval-banner-copy {
|
|
1412
1415
|
display: grid;
|
|
1413
1416
|
gap: 0.28rem;
|
|
1417
|
+
min-height: 0;
|
|
1418
|
+
overflow-y: auto;
|
|
1419
|
+
overflow-x: hidden;
|
|
1420
|
+
overscroll-behavior: contain;
|
|
1421
|
+
scrollbar-gutter: stable;
|
|
1422
|
+
-webkit-overflow-scrolling: touch;
|
|
1423
|
+
touch-action: pan-y;
|
|
1424
|
+
padding-right: 0.18rem;
|
|
1414
1425
|
}
|
|
1415
1426
|
|
|
1416
1427
|
.approval-banner-head {
|
|
@@ -1496,6 +1507,9 @@ button:hover {
|
|
|
1496
1507
|
display: flex;
|
|
1497
1508
|
gap: 0.55rem;
|
|
1498
1509
|
flex-wrap: wrap;
|
|
1510
|
+
flex-shrink: 0;
|
|
1511
|
+
padding-top: 0.15rem;
|
|
1512
|
+
border-top: 1px solid rgba(188, 90, 48, 0.12);
|
|
1499
1513
|
}
|
|
1500
1514
|
|
|
1501
1515
|
.approval-banner-actions .primary-button,
|
|
@@ -5104,6 +5118,13 @@ button:hover {
|
|
|
5104
5118
|
padding-right: env(safe-area-inset-right, 0px);
|
|
5105
5119
|
}
|
|
5106
5120
|
|
|
5121
|
+
.composer-panel:has(#session-approval-slot .approval-banner) {
|
|
5122
|
+
max-height: calc(100dvh - env(safe-area-inset-top, 0px) - 3rem);
|
|
5123
|
+
overflow-y: auto;
|
|
5124
|
+
overscroll-behavior: contain;
|
|
5125
|
+
-webkit-overflow-scrolling: touch;
|
|
5126
|
+
}
|
|
5127
|
+
|
|
5107
5128
|
.composer-dock {
|
|
5108
5129
|
max-width: 100%;
|
|
5109
5130
|
}
|
|
@@ -5112,6 +5133,7 @@ button:hover {
|
|
|
5112
5133
|
gap: 0.45rem;
|
|
5113
5134
|
margin-bottom: 0.45rem;
|
|
5114
5135
|
padding: 0.56rem 0.62rem;
|
|
5136
|
+
max-height: min(40dvh, 18rem);
|
|
5115
5137
|
}
|
|
5116
5138
|
}
|
|
5117
5139
|
|
package/LICENSE
DELETED
|
@@ -1,21 +0,0 @@
|
|
|
1
|
-
MIT License
|
|
2
|
-
|
|
3
|
-
Copyright (c) 2026 RemCodex contributors
|
|
4
|
-
|
|
5
|
-
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
-
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
-
in the Software without restriction, including without limitation the rights
|
|
8
|
-
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
-
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
-
furnished to do so, subject to the following conditions:
|
|
11
|
-
|
|
12
|
-
The above copyright notice and this permission notice shall be included in all
|
|
13
|
-
copies or substantial portions of the Software.
|
|
14
|
-
|
|
15
|
-
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
-
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
-
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
-
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
-
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
-
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
-
SOFTWARE.
|