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 CHANGED
@@ -1,103 +1,45 @@
1
- # 🚀 RemCodex
1
+ # RemCodex
2
2
 
3
- > Control your AI coding agent from anywhere.
4
- > Built for Codex. Ready for more.
3
+ > Control Codex from anywhere. Even on your phone.
5
4
 
6
- Run Codex once on your computer.
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
- > Not a wrapper. Not a proxy.
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
- ![RemCodex hero cover](docs/assets/hero-cover.png)
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
- ## What is RemCodex?
15
+ This project is currently a **beta / developer preview**.
18
16
 
19
- RemCodex is a **local-first control layer for Codex**.
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
- It turns AI execution into a **controllable system** —
22
- visible, interruptible, and continuous.
19
+ ## Why People Use It
23
20
 
24
- - 👀 See what the AI is doing in real time
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
- > One session. Any device.
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
- ## 🎬 A real workflow
31
+ ## What It Does
35
32
 
36
- You start a long Codex session on your machine.
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
- Then you leave your desk.
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
  ![RemCodex desktop workspace](docs/assets/hero-desktop.png)
103
45
 
@@ -113,203 +55,249 @@ http://<your-ip>:18840
113
55
 
114
56
  ![RemCodex imported Codex session](docs/assets/imported-session.png)
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
- RemCodex is a **browser-based workspace for Codex sessions**.
60
+ ## Who It Is For
123
61
 
124
- It is built for real workflows:
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
- - long-running sessions
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
- Instead of raw terminal logs, you get a structured, visual timeline you can follow and control.
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 timeline
141
- - Fixed input composer
78
+ - Right-side timeline / execution flow for the active session
79
+ - Fixed composer at the bottom
142
80
  - Semantic timeline rendering for:
143
- - user messages
144
- - assistant output
145
- - thinking
146
- - commands
147
- - patches
148
- - approvals
149
- - system events
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
- ### Timeline
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
- - Semantic rendering (not raw logs)
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
- ### Imported sessions
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
- - Import from `~/.codex/sessions/...`
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
- ## 🧠 Architecture
109
+ For the current developer preview, the recommended local install path is:
182
110
 
183
- ```
184
- Codex CLI → Event stream → Semantic layer → Timeline → Web UI
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
- ## ⚙️ Requirements
120
+ ```text
121
+ http://127.0.0.1:3000
122
+ ```
190
123
 
191
- - Node.js
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
- ## ⚙️ Configuration
128
+ RemCodex already ships with a local CLI entrypoint, even though the npm package is not published yet.
197
129
 
198
- Default port: **18840**
130
+ If you do not want to run `npm link`, you can call the built CLI directly:
199
131
 
200
132
  ```bash
201
- PORT=18841 npx remcodex
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
- Make sure you have:
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
- - `python3`
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
- ### Debug install issues
151
+ Planned install target after the npm package is published:
224
152
 
225
153
  ```bash
226
- npm install -g remcodex
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
- npx remcodex --no-open
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
- ## 🔧 How it works
168
+ At a high level:
242
169
 
243
- 1. Codex emits events
244
- 2. Backend stores them (SQLite)
245
- 3. Frontend loads timeline snapshot
246
- 4. Live updates stream via WebSocket
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
- Result:
175
+ This gives the UI:
249
176
 
177
+ - smooth streaming
250
178
  - recoverable sessions
251
- - real-time UI
252
- - consistent execution flow
179
+ - imported rollout support
180
+ - a consistent execution timeline instead of raw terminal logs
253
181
 
254
- ---
182
+ ## Key Behaviors
255
183
 
256
- ## 📊 Status
184
+ ### Approvals
257
185
 
258
- - Beta / developer preview
259
- - Local-first architecture
260
- - No cloud dependency
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
- ## 🗺 Roadmap
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
- **Visibility**
200
+ ### Timeline and Execution Flow
267
201
 
268
- - fully observable execution
269
- - clear action timeline
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
- **Control**
207
+ ## Configuration
272
208
 
273
- - fine-grained approvals
274
- - safer execution
209
+ Supported environment variables:
275
210
 
276
- **Continuity**
211
+ - `PORT`
212
+ - `DATABASE_PATH`
213
+ - `PROJECT_ROOTS`
214
+ - `CODEX_COMMAND`
215
+ - `CODEX_MODE`
277
216
 
278
- - survive refresh / sleep
279
- - stable long runs
217
+ For launch screenshots or demo data, you can rebuild a clean demo database with:
280
218
 
281
- **Access**
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
- - control from any device
255
+ ## Main Endpoints
284
256
 
285
- **Integration**
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
- - IDE integrations
288
- - optional sharing
271
+ ## What Is Not Finished Yet
289
272
 
290
- ---
273
+ This is the honest list:
291
274
 
292
- ## 👥 Who it’s for
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
- - developers already using Codex
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
- ## ⚠️ What’s not finished yet
285
+ Near-term:
302
286
 
303
- - no polished installer yet
304
- - no desktop packaging
305
- - no production-grade auth
306
- - no release pipeline
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
- If you're comfortable running a local Node app —
309
- you can use it today.
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
- ## 📄 License
301
+ No license has been added yet.
314
302
 
315
- MIT License
303
+ Until a license is added, assume this repository is **source-available for review only**, not open source for reuse.
@@ -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 ?? String(exports.DEFAULT_PORT), 10);
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();
@@ -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 ?? String(app_1.DEFAULT_PORT), 10);
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.5",
3
+ "version": "0.1.0-beta.7",
4
4
  "description": "Control Codex from anywhere. Even on your phone.",
5
- "license": "MIT",
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: true,
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
- <button type="button" class="secondary-button" data-approval-decision="decline" ${canResolve ? "" : "disabled"}>${escapeHtml(t("approval.deny"))}</button>
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 || banner.getAttribute("data-approval-resumable") === "false") {
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
 
@@ -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.