remcodex 0.1.0-beta.1 → 0.1.0-beta.11
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 +30 -39
- package/dist/server/src/app.js +3 -26
- package/dist/server/src/cli.js +65 -7
- package/dist/server/src/controllers/session.controller.js +12 -0
- package/dist/server/src/db/migrations.js +30 -1
- package/dist/server/src/db/schema.sql +48 -0
- package/dist/server/src/services/codex-rollout-sync.js +5 -16
- package/dist/server/src/services/event-store.js +28 -5
- package/dist/server/src/services/session-manager.js +79 -6
- package/dist/server/src/services/session-timeline-service.js +7 -169
- package/dist/server/src/utils/output-limits.js +73 -0
- package/dist/server/src/utils/runtime-paths.js +31 -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 +13 -5
- package/scripts/check-node-version.js +14 -0
- package/web/api.js +7 -0
- package/web/app.js +241 -37
- package/web/i18n/locales/en.js +3 -2
- package/web/i18n/locales/zh-CN.js +3 -2
- package/web/session-timeline-reducer.js +66 -13
- package/web/styles.css +22 -0
- package/LICENSE +0 -21
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,
|
|
@@ -589,6 +590,7 @@ function getPendingApprovalFromTimelineState(timelineState) {
|
|
|
589
590
|
.filter(
|
|
590
591
|
(item) =>
|
|
591
592
|
item?.status === "pending" &&
|
|
593
|
+
!isApprovalDismissed(sessionId, item?.requestId) &&
|
|
592
594
|
!isApprovalSuppressed(sessionId, item?.requestId),
|
|
593
595
|
)
|
|
594
596
|
.sort((left, right) => Number(right?.seq || 0) - Number(left?.seq || 0))[0];
|
|
@@ -613,14 +615,11 @@ function resolveDetailPendingApproval(session, timelineState) {
|
|
|
613
615
|
const sessionPending = session?.pendingApproval || null;
|
|
614
616
|
const liveBusy = session?.liveBusy === true;
|
|
615
617
|
const sessionId = String(session?.sessionId || "").trim();
|
|
616
|
-
|
|
617
|
-
if (!liveBusy) {
|
|
618
|
-
return null;
|
|
619
|
-
}
|
|
618
|
+
const canResolve = liveBusy && sessionPending?.resumable !== false;
|
|
620
619
|
|
|
621
620
|
if (!timelinePending) {
|
|
622
621
|
return sessionPending &&
|
|
623
|
-
sessionPending.
|
|
622
|
+
!isApprovalDismissed(sessionId, sessionPending.requestId, sessionPending.callId) &&
|
|
624
623
|
!isApprovalSuppressed(sessionId, sessionPending.requestId, sessionPending.callId)
|
|
625
624
|
? sessionPending
|
|
626
625
|
: null;
|
|
@@ -629,7 +628,7 @@ function resolveDetailPendingApproval(session, timelineState) {
|
|
|
629
628
|
return {
|
|
630
629
|
...timelinePending,
|
|
631
630
|
...(sessionPending && sessionPending.requestId === timelinePending.requestId ? sessionPending : {}),
|
|
632
|
-
resumable:
|
|
631
|
+
resumable: canResolve,
|
|
633
632
|
};
|
|
634
633
|
}
|
|
635
634
|
|
|
@@ -653,6 +652,37 @@ function isApprovalSuppressed(sessionId, requestId, callId = "") {
|
|
|
653
652
|
return true;
|
|
654
653
|
}
|
|
655
654
|
|
|
655
|
+
function getApprovalDismissalKey(sessionId, requestId) {
|
|
656
|
+
return `${String(sessionId || "").trim()}:${String(requestId || "").trim()}`;
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
function isApprovalDismissed(sessionId, requestId) {
|
|
660
|
+
const key = getApprovalDismissalKey(sessionId, requestId);
|
|
661
|
+
if (!key || key === ":") {
|
|
662
|
+
return false;
|
|
663
|
+
}
|
|
664
|
+
return Boolean(state.detail.dismissedApprovalKeys?.[key]);
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
function dismissApproval(sessionId, requestId) {
|
|
668
|
+
const key = getApprovalDismissalKey(sessionId, requestId);
|
|
669
|
+
if (!key || key === ":") {
|
|
670
|
+
return;
|
|
671
|
+
}
|
|
672
|
+
state.detail.dismissedApprovalKeys = {
|
|
673
|
+
...(state.detail.dismissedApprovalKeys || {}),
|
|
674
|
+
[key]: true,
|
|
675
|
+
};
|
|
676
|
+
}
|
|
677
|
+
|
|
678
|
+
function isTerminalApprovalError(error) {
|
|
679
|
+
const message = messageOf(error);
|
|
680
|
+
return (
|
|
681
|
+
message === "Approval request not found." ||
|
|
682
|
+
message === "Approval request can no longer be resumed."
|
|
683
|
+
);
|
|
684
|
+
}
|
|
685
|
+
|
|
656
686
|
function clearResolvingApprovalState() {
|
|
657
687
|
state.detail.resolvingApprovalRequestId = "";
|
|
658
688
|
state.detail.resolvingApprovalSessionId = "";
|
|
@@ -671,7 +701,10 @@ function syncDetailPendingApproval(session = state.detail.session, timelineState
|
|
|
671
701
|
const sessionPending = session?.pendingApproval || null;
|
|
672
702
|
const timelinePending = timelineState?.approvalsByRequestId
|
|
673
703
|
? Object.values(timelineState.approvalsByRequestId).some(
|
|
674
|
-
(item) =>
|
|
704
|
+
(item) =>
|
|
705
|
+
item?.status === "pending" &&
|
|
706
|
+
!isApprovalDismissed(session?.sessionId, item?.requestId) &&
|
|
707
|
+
isApprovalSuppressed(session?.sessionId, item?.requestId),
|
|
675
708
|
)
|
|
676
709
|
: false;
|
|
677
710
|
const detailPending =
|
|
@@ -683,6 +716,7 @@ function syncDetailPendingApproval(session = state.detail.session, timelineState
|
|
|
683
716
|
);
|
|
684
717
|
const sessionStillPending =
|
|
685
718
|
sessionPending &&
|
|
719
|
+
!isApprovalDismissed(session?.sessionId, sessionPending.requestId, sessionPending.callId) &&
|
|
686
720
|
isApprovalSuppressed(session?.sessionId, sessionPending.requestId, sessionPending.callId);
|
|
687
721
|
|
|
688
722
|
if (!timelinePending && !detailPending && !sessionStillPending) {
|
|
@@ -697,6 +731,20 @@ function mergeDetailTimelineRawEvents(nextRawEvents) {
|
|
|
697
731
|
return;
|
|
698
732
|
}
|
|
699
733
|
|
|
734
|
+
const activeSessionId = getActiveDetailSessionId();
|
|
735
|
+
if (!activeSessionId) {
|
|
736
|
+
return;
|
|
737
|
+
}
|
|
738
|
+
|
|
739
|
+
const filteredRawEvents = nextRawEvents.filter((rawEvent) => {
|
|
740
|
+
const eventSessionId = String(rawEvent?.sessionId || rawEvent?.session_id || "").trim();
|
|
741
|
+
return !eventSessionId || eventSessionId === activeSessionId;
|
|
742
|
+
});
|
|
743
|
+
|
|
744
|
+
if (filteredRawEvents.length === 0) {
|
|
745
|
+
return;
|
|
746
|
+
}
|
|
747
|
+
|
|
700
748
|
const existingIds = new Set(state.detail.rawEvents.map((event) => event.id));
|
|
701
749
|
const currentMaxSeq = state.detail.rawEvents.reduce(
|
|
702
750
|
(maxSeq, event) => Math.max(maxSeq, Number(event?.seq || 0)),
|
|
@@ -705,7 +753,7 @@ function mergeDetailTimelineRawEvents(nextRawEvents) {
|
|
|
705
753
|
const appended = [];
|
|
706
754
|
let canApplyIncrementally = true;
|
|
707
755
|
|
|
708
|
-
|
|
756
|
+
filteredRawEvents.forEach((rawEvent) => {
|
|
709
757
|
if (!rawEvent?.id || existingIds.has(rawEvent.id)) {
|
|
710
758
|
return;
|
|
711
759
|
}
|
|
@@ -740,19 +788,41 @@ function mergeDetailTimelineRawEvents(nextRawEvents) {
|
|
|
740
788
|
syncDetailPendingApproval(state.detail.session, state.detail.timelineState);
|
|
741
789
|
}
|
|
742
790
|
|
|
791
|
+
function getActiveDetailSessionId() {
|
|
792
|
+
return String(state.workspace.activeSessionId || state.detail.session?.sessionId || "").trim();
|
|
793
|
+
}
|
|
794
|
+
|
|
795
|
+
function isActiveDetailSession(sessionId) {
|
|
796
|
+
const normalizedSessionId = String(sessionId || "").trim();
|
|
797
|
+
return Boolean(normalizedSessionId) && getActiveDetailSessionId() === normalizedSessionId;
|
|
798
|
+
}
|
|
799
|
+
|
|
743
800
|
async function catchUpSessionEvents(sessionId, afterSeq) {
|
|
801
|
+
const normalizedSessionId = String(sessionId || "").trim();
|
|
744
802
|
let nextAfter = Number(afterSeq || 0);
|
|
745
|
-
if (!nextAfter) {
|
|
803
|
+
if (!normalizedSessionId || !nextAfter || !isActiveDetailSession(normalizedSessionId)) {
|
|
746
804
|
return;
|
|
747
805
|
}
|
|
748
806
|
|
|
749
807
|
for (let page = 0; page < 10; page += 1) {
|
|
750
|
-
|
|
808
|
+
if (!isActiveDetailSession(normalizedSessionId)) {
|
|
809
|
+
return;
|
|
810
|
+
}
|
|
811
|
+
|
|
812
|
+
const payload = await getSessionEvents(normalizedSessionId, {
|
|
751
813
|
after: nextAfter,
|
|
752
814
|
limit: 200,
|
|
753
815
|
});
|
|
816
|
+
if (!isActiveDetailSession(normalizedSessionId)) {
|
|
817
|
+
return;
|
|
818
|
+
}
|
|
754
819
|
|
|
755
|
-
const items = Array.isArray(payload?.items)
|
|
820
|
+
const items = Array.isArray(payload?.items)
|
|
821
|
+
? payload.items.filter((item) => {
|
|
822
|
+
const eventSessionId = String(item?.sessionId || item?.session_id || "").trim();
|
|
823
|
+
return !eventSessionId || eventSessionId === normalizedSessionId;
|
|
824
|
+
})
|
|
825
|
+
: [];
|
|
756
826
|
if (items.length === 0) {
|
|
757
827
|
return;
|
|
758
828
|
}
|
|
@@ -1326,6 +1396,7 @@ const state = {
|
|
|
1326
1396
|
codexStatus: null,
|
|
1327
1397
|
codexQuota: null,
|
|
1328
1398
|
pendingApproval: null,
|
|
1399
|
+
dismissedApprovalKeys: {},
|
|
1329
1400
|
resolvingApprovalRequestId: "",
|
|
1330
1401
|
resolvingApprovalSessionId: "",
|
|
1331
1402
|
resolvingApprovalCallId: "",
|
|
@@ -1347,6 +1418,7 @@ const state = {
|
|
|
1347
1418
|
inspectDrawerOpen: false,
|
|
1348
1419
|
inspectSelectionKey: "",
|
|
1349
1420
|
renderTimerId: 0,
|
|
1421
|
+
loadRequestId: 0,
|
|
1350
1422
|
resumeSyncInFlight: false,
|
|
1351
1423
|
lastResumeSyncAt: 0,
|
|
1352
1424
|
...DEFAULT_DETAIL_VIEW,
|
|
@@ -1369,6 +1441,7 @@ window.addEventListener("pageshow", () => {
|
|
|
1369
1441
|
});
|
|
1370
1442
|
|
|
1371
1443
|
function renderRoute() {
|
|
1444
|
+
state.detail.loadRequestId = Number(state.detail.loadRequestId || 0) + 1;
|
|
1372
1445
|
cleanupSocket();
|
|
1373
1446
|
cleanupDetailClock();
|
|
1374
1447
|
cleanupLiveResumeSync();
|
|
@@ -3048,30 +3121,50 @@ function renderSessionsList() {
|
|
|
3048
3121
|
}
|
|
3049
3122
|
|
|
3050
3123
|
async function renderSessionDetailPage(sessionId) {
|
|
3051
|
-
|
|
3124
|
+
const normalizedSessionId = String(sessionId || "").trim();
|
|
3125
|
+
if (!normalizedSessionId) {
|
|
3126
|
+
return;
|
|
3127
|
+
}
|
|
3128
|
+
|
|
3129
|
+
const previousSessionId = String(state.detail.session?.sessionId || "").trim();
|
|
3130
|
+
if (previousSessionId !== normalizedSessionId) {
|
|
3131
|
+
state.detail.dismissedApprovalKeys = {};
|
|
3132
|
+
}
|
|
3133
|
+
|
|
3134
|
+
state.workspace.activeSessionId = normalizedSessionId;
|
|
3135
|
+
const loadRequestId = Number(state.detail.loadRequestId || 0) + 1;
|
|
3136
|
+
state.detail.loadRequestId = loadRequestId;
|
|
3137
|
+
const isStaleDetailLoad = () =>
|
|
3138
|
+
state.detail.loadRequestId !== loadRequestId || state.workspace.activeSessionId !== normalizedSessionId;
|
|
3052
3139
|
const mainSlot = document.querySelector("#workspace-main-slot");
|
|
3053
3140
|
if (mainSlot) {
|
|
3054
3141
|
mainSlot.innerHTML = loadingCard(t("workspace.loading.session"));
|
|
3055
3142
|
} else {
|
|
3056
3143
|
app.innerHTML = renderWorkspaceShell({
|
|
3057
|
-
sidebarHtml: renderWorkspaceSidebar(
|
|
3144
|
+
sidebarHtml: renderWorkspaceSidebar(normalizedSessionId),
|
|
3058
3145
|
mainHtml: loadingCard(t("workspace.loading.session")),
|
|
3059
3146
|
});
|
|
3060
3147
|
syncWorkspaceShellState();
|
|
3061
3148
|
bindWorkspaceCreateDialogControls();
|
|
3062
3149
|
bindWorkspaceImportDialogControls();
|
|
3063
|
-
bindWorkspaceSidebarControls(
|
|
3150
|
+
bindWorkspaceSidebarControls(normalizedSessionId);
|
|
3064
3151
|
}
|
|
3065
3152
|
|
|
3066
3153
|
try {
|
|
3067
|
-
await syncImportedSession(
|
|
3154
|
+
await syncImportedSession(normalizedSessionId).catch(() => null);
|
|
3155
|
+
if (isStaleDetailLoad()) {
|
|
3156
|
+
return;
|
|
3157
|
+
}
|
|
3068
3158
|
|
|
3069
3159
|
const [session, eventData, uiOptionsResult, hostsResult] = await Promise.all([
|
|
3070
|
-
getSession(
|
|
3071
|
-
loadInitialSessionEvents(
|
|
3160
|
+
getSession(normalizedSessionId),
|
|
3161
|
+
loadInitialSessionEvents(normalizedSessionId),
|
|
3072
3162
|
getCodexUiOptions().catch(() => null),
|
|
3073
3163
|
getCodexHosts().catch(() => null),
|
|
3074
3164
|
]);
|
|
3165
|
+
if (isStaleDetailLoad()) {
|
|
3166
|
+
return;
|
|
3167
|
+
}
|
|
3075
3168
|
|
|
3076
3169
|
const uiOptions =
|
|
3077
3170
|
uiOptionsResult &&
|
|
@@ -3081,6 +3174,14 @@ async function renderSessionDetailPage(sessionId) {
|
|
|
3081
3174
|
uiOptionsResult.reasoningLevels.length > 0
|
|
3082
3175
|
? uiOptionsResult
|
|
3083
3176
|
: CLIENT_FALLBACK_CODEX_UI_OPTIONS;
|
|
3177
|
+
const codexStatus = await getCodexStatus({
|
|
3178
|
+
sessionId: normalizedSessionId,
|
|
3179
|
+
threadId: session.codexThreadId || "",
|
|
3180
|
+
cwd: session.projectPath || "",
|
|
3181
|
+
}).catch(() => null);
|
|
3182
|
+
if (isStaleDetailLoad()) {
|
|
3183
|
+
return;
|
|
3184
|
+
}
|
|
3084
3185
|
|
|
3085
3186
|
state.detail.session = session;
|
|
3086
3187
|
replaceDetailTimelineRawEvents(eventData.items);
|
|
@@ -3124,12 +3225,8 @@ async function renderSessionDetailPage(sessionId) {
|
|
|
3124
3225
|
state.detail.inspectDrawerOpen = false;
|
|
3125
3226
|
state.detail.inspectSelectionKey = "";
|
|
3126
3227
|
state.detail.optimisticSend = null;
|
|
3127
|
-
state.detail.codexQuota = readCachedCodexQuota(
|
|
3128
|
-
state.detail.codexStatus =
|
|
3129
|
-
sessionId,
|
|
3130
|
-
threadId: session.codexThreadId || "",
|
|
3131
|
-
cwd: session.projectPath || "",
|
|
3132
|
-
}).catch(() => null);
|
|
3228
|
+
state.detail.codexQuota = readCachedCodexQuota(normalizedSessionId);
|
|
3229
|
+
state.detail.codexStatus = codexStatus;
|
|
3133
3230
|
state.socketState = "connecting";
|
|
3134
3231
|
|
|
3135
3232
|
const detailQuery = parseHashRoute(window.location.hash || "").query || "";
|
|
@@ -3139,26 +3236,36 @@ async function renderSessionDetailPage(sessionId) {
|
|
|
3139
3236
|
}
|
|
3140
3237
|
|
|
3141
3238
|
renderSessionDetail();
|
|
3142
|
-
|
|
3143
|
-
|
|
3239
|
+
if (isStaleDetailLoad()) {
|
|
3240
|
+
return;
|
|
3241
|
+
}
|
|
3242
|
+
|
|
3243
|
+
attachSessionSocket(normalizedSessionId);
|
|
3244
|
+
void catchUpSessionEvents(normalizedSessionId, state.detail.cursor)
|
|
3144
3245
|
.then(() => {
|
|
3145
|
-
|
|
3246
|
+
if (!isStaleDetailLoad()) {
|
|
3247
|
+
scheduleSessionDetailRender();
|
|
3248
|
+
}
|
|
3146
3249
|
})
|
|
3147
3250
|
.catch(() => null);
|
|
3148
|
-
scheduleImportedSessionSync(
|
|
3251
|
+
scheduleImportedSessionSync(normalizedSessionId);
|
|
3149
3252
|
} catch (error) {
|
|
3253
|
+
if (isStaleDetailLoad()) {
|
|
3254
|
+
return;
|
|
3255
|
+
}
|
|
3256
|
+
|
|
3150
3257
|
const nextMainSlot = document.querySelector("#workspace-main-slot");
|
|
3151
3258
|
if (nextMainSlot) {
|
|
3152
3259
|
nextMainSlot.innerHTML = errorCard(messageOf(error));
|
|
3153
3260
|
} else {
|
|
3154
3261
|
app.innerHTML = renderWorkspaceShell({
|
|
3155
|
-
sidebarHtml: renderWorkspaceSidebar(
|
|
3262
|
+
sidebarHtml: renderWorkspaceSidebar(normalizedSessionId),
|
|
3156
3263
|
mainHtml: errorCard(messageOf(error)),
|
|
3157
3264
|
});
|
|
3158
3265
|
syncWorkspaceShellState();
|
|
3159
3266
|
bindWorkspaceCreateDialogControls();
|
|
3160
3267
|
bindWorkspaceImportDialogControls();
|
|
3161
|
-
bindWorkspaceSidebarControls(
|
|
3268
|
+
bindWorkspaceSidebarControls(normalizedSessionId);
|
|
3162
3269
|
}
|
|
3163
3270
|
}
|
|
3164
3271
|
}
|
|
@@ -3674,14 +3781,33 @@ function scheduleSessionDetailRender(options = {}) {
|
|
|
3674
3781
|
}
|
|
3675
3782
|
|
|
3676
3783
|
function attachSessionSocket(sessionId) {
|
|
3677
|
-
|
|
3784
|
+
const normalizedSessionId = String(sessionId || "").trim();
|
|
3785
|
+
if (!normalizedSessionId) {
|
|
3786
|
+
return;
|
|
3787
|
+
}
|
|
3788
|
+
|
|
3789
|
+
cleanupSocket();
|
|
3790
|
+
const socket = connectSessionSocket(normalizedSessionId, {
|
|
3678
3791
|
onStateChange(nextState) {
|
|
3792
|
+
if (state.ws !== socket || !isActiveDetailSession(normalizedSessionId)) {
|
|
3793
|
+
return;
|
|
3794
|
+
}
|
|
3795
|
+
|
|
3679
3796
|
state.socketState = nextState;
|
|
3680
3797
|
if (state.detail.session) {
|
|
3681
3798
|
scheduleSessionDetailRender();
|
|
3682
3799
|
}
|
|
3683
3800
|
},
|
|
3684
3801
|
onEvent(event) {
|
|
3802
|
+
const eventSessionId = String(event?.sessionId || event?.session_id || "").trim();
|
|
3803
|
+
if (
|
|
3804
|
+
state.ws !== socket ||
|
|
3805
|
+
!isActiveDetailSession(normalizedSessionId) ||
|
|
3806
|
+
(eventSessionId && eventSessionId !== normalizedSessionId)
|
|
3807
|
+
) {
|
|
3808
|
+
return;
|
|
3809
|
+
}
|
|
3810
|
+
|
|
3685
3811
|
if (state.detail.session) {
|
|
3686
3812
|
state.detail.session.updatedAt = new Date().toISOString();
|
|
3687
3813
|
if (event.type === "turn.started") {
|
|
@@ -3705,12 +3831,12 @@ function attachSessionSocket(sessionId) {
|
|
|
3705
3831
|
event.content.startsWith("Codex thread started: ")
|
|
3706
3832
|
) {
|
|
3707
3833
|
state.detail.session.codexThreadId = event.content.slice("Codex thread started: ".length);
|
|
3708
|
-
refreshCodexStatus(
|
|
3834
|
+
refreshCodexStatus(normalizedSessionId);
|
|
3709
3835
|
}
|
|
3710
3836
|
}
|
|
3711
3837
|
|
|
3712
3838
|
if ((event.type === "token_count" || event.type === "codex.quota") && state.detail.session) {
|
|
3713
|
-
setDetailCodexQuota(
|
|
3839
|
+
setDetailCodexQuota(normalizedSessionId, event.payload);
|
|
3714
3840
|
}
|
|
3715
3841
|
|
|
3716
3842
|
trackUnseenEvents([event]);
|
|
@@ -3719,6 +3845,7 @@ function attachSessionSocket(sessionId) {
|
|
|
3719
3845
|
scheduleSessionDetailRender();
|
|
3720
3846
|
},
|
|
3721
3847
|
});
|
|
3848
|
+
state.ws = socket;
|
|
3722
3849
|
}
|
|
3723
3850
|
|
|
3724
3851
|
async function refreshCodexStatus(sessionId) {
|
|
@@ -7041,6 +7168,15 @@ function renderPendingApprovalBar(detailState) {
|
|
|
7041
7168
|
const restoreHint = !canResolve
|
|
7042
7169
|
? `<p class="approval-banner-meta approval-banner-meta--warning">${escapeHtml(t("approval.restoreHint"))}</p>`
|
|
7043
7170
|
: "";
|
|
7171
|
+
const actionHtml = canResolve
|
|
7172
|
+
? `
|
|
7173
|
+
<button type="button" class="secondary-button" data-approval-decision="decline">${escapeHtml(t("approval.deny"))}</button>
|
|
7174
|
+
<button type="button" class="secondary-button" data-approval-decision="accept">${escapeHtml(t("approval.allowOnce"))}</button>
|
|
7175
|
+
<button type="button" class="primary-button" data-approval-decision="acceptForSession">${escapeHtml(t("approval.allowForTurn"))}</button>
|
|
7176
|
+
`
|
|
7177
|
+
: `
|
|
7178
|
+
<button type="button" class="primary-button" data-approval-retry="true">${escapeHtml(t("approval.retryAction"))}</button>
|
|
7179
|
+
`;
|
|
7044
7180
|
|
|
7045
7181
|
return `
|
|
7046
7182
|
<section class="approval-banner" data-approval-id="${escapeHtml(approval.requestId)}" data-approval-resumable="${canResolve ? "true" : "false"}">
|
|
@@ -7073,9 +7209,7 @@ function renderPendingApprovalBar(detailState) {
|
|
|
7073
7209
|
${restoreHint}
|
|
7074
7210
|
</div>
|
|
7075
7211
|
<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>
|
|
7212
|
+
${actionHtml}
|
|
7079
7213
|
</div>
|
|
7080
7214
|
</section>
|
|
7081
7215
|
`;
|
|
@@ -7083,7 +7217,66 @@ function renderPendingApprovalBar(detailState) {
|
|
|
7083
7217
|
|
|
7084
7218
|
function bindPendingApprovalControls(sessionId) {
|
|
7085
7219
|
const banner = document.querySelector("#session-approval-slot .approval-banner");
|
|
7086
|
-
if (!banner
|
|
7220
|
+
if (!banner) {
|
|
7221
|
+
return;
|
|
7222
|
+
}
|
|
7223
|
+
|
|
7224
|
+
const retryButton = banner.querySelector("[data-approval-retry]");
|
|
7225
|
+
if (retryButton instanceof HTMLButtonElement) {
|
|
7226
|
+
retryButton.onclick = async () => {
|
|
7227
|
+
const approval = state.detail.pendingApproval;
|
|
7228
|
+
const requestId = banner.getAttribute("data-approval-id");
|
|
7229
|
+
if (!approval || !requestId) {
|
|
7230
|
+
return;
|
|
7231
|
+
}
|
|
7232
|
+
|
|
7233
|
+
const previousPendingApproval = { ...approval };
|
|
7234
|
+
const previousStatus = state.detail.session?.status || "waiting_input";
|
|
7235
|
+
const previousLiveBusy = Boolean(state.detail.session?.liveBusy);
|
|
7236
|
+
retryButton.disabled = true;
|
|
7237
|
+
banner.setAttribute("aria-busy", "true");
|
|
7238
|
+
state.detail.pendingApproval = null;
|
|
7239
|
+
if (state.detail.session?.sessionId === sessionId) {
|
|
7240
|
+
state.detail.session.status = "running";
|
|
7241
|
+
state.detail.session.liveBusy = true;
|
|
7242
|
+
}
|
|
7243
|
+
scheduleSessionDetailRender({ immediate: true });
|
|
7244
|
+
|
|
7245
|
+
try {
|
|
7246
|
+
const codex = buildCodexLaunchPayload(
|
|
7247
|
+
state.detail.codexLaunch,
|
|
7248
|
+
state.detail.codexUiOptions,
|
|
7249
|
+
);
|
|
7250
|
+
const payload = codex ? { codex } : {};
|
|
7251
|
+
await retrySessionApproval(sessionId, requestId, payload);
|
|
7252
|
+
await resumeActiveSessionDetail("approval-retry");
|
|
7253
|
+
} catch (error) {
|
|
7254
|
+
if (isTerminalApprovalError(error)) {
|
|
7255
|
+
dismissApproval(sessionId, requestId);
|
|
7256
|
+
state.detail.pendingApproval = null;
|
|
7257
|
+
const refreshedSession = await getSession(sessionId).catch(() => null);
|
|
7258
|
+
if (refreshedSession && state.detail.session?.sessionId === sessionId) {
|
|
7259
|
+
state.detail.session = refreshedSession;
|
|
7260
|
+
updateSessionListItem(refreshedSession);
|
|
7261
|
+
} else if (state.detail.session?.sessionId === sessionId) {
|
|
7262
|
+
state.detail.session.status = previousStatus;
|
|
7263
|
+
state.detail.session.liveBusy = previousLiveBusy;
|
|
7264
|
+
}
|
|
7265
|
+
syncDetailPendingApproval(state.detail.session, state.detail.timelineState);
|
|
7266
|
+
} else {
|
|
7267
|
+
state.detail.pendingApproval = previousPendingApproval;
|
|
7268
|
+
if (state.detail.session?.sessionId === sessionId) {
|
|
7269
|
+
state.detail.session.status = previousStatus;
|
|
7270
|
+
state.detail.session.liveBusy = previousLiveBusy;
|
|
7271
|
+
}
|
|
7272
|
+
}
|
|
7273
|
+
scheduleSessionDetailRender({ immediate: true });
|
|
7274
|
+
showToast(messageOf(error));
|
|
7275
|
+
}
|
|
7276
|
+
};
|
|
7277
|
+
}
|
|
7278
|
+
|
|
7279
|
+
if (banner.getAttribute("data-approval-resumable") === "false") {
|
|
7087
7280
|
return;
|
|
7088
7281
|
}
|
|
7089
7282
|
|
|
@@ -7125,7 +7318,18 @@ function bindPendingApprovalControls(sessionId) {
|
|
|
7125
7318
|
if (isApprovalSuppressed(sessionId, requestId, previousPendingApproval?.callId)) {
|
|
7126
7319
|
clearResolvingApprovalState();
|
|
7127
7320
|
}
|
|
7128
|
-
|
|
7321
|
+
if (isTerminalApprovalError(error)) {
|
|
7322
|
+
dismissApproval(sessionId, requestId);
|
|
7323
|
+
state.detail.pendingApproval = null;
|
|
7324
|
+
const refreshedSession = await getSession(sessionId).catch(() => null);
|
|
7325
|
+
if (refreshedSession && state.detail.session?.sessionId === sessionId) {
|
|
7326
|
+
state.detail.session = refreshedSession;
|
|
7327
|
+
updateSessionListItem(refreshedSession);
|
|
7328
|
+
}
|
|
7329
|
+
syncDetailPendingApproval(state.detail.session, state.detail.timelineState);
|
|
7330
|
+
} else {
|
|
7331
|
+
state.detail.pendingApproval = previousPendingApproval;
|
|
7332
|
+
}
|
|
7129
7333
|
showToast(messageOf(error));
|
|
7130
7334
|
scheduleSessionDetailRender({ immediate: true });
|
|
7131
7335
|
}
|
package/web/i18n/locales/en.js
CHANGED
|
@@ -64,9 +64,9 @@ export default {
|
|
|
64
64
|
"projects.registryEyebrow": "Project registry",
|
|
65
65
|
"projects.addTitle": "Add project",
|
|
66
66
|
"projects.name": "Project name",
|
|
67
|
-
"projects.namePlaceholder": "e.g.
|
|
67
|
+
"projects.namePlaceholder": "e.g. my-service",
|
|
68
68
|
"projects.path": "Local path",
|
|
69
|
-
"projects.pathPlaceholder": "/workspace/
|
|
69
|
+
"projects.pathPlaceholder": "/workspace/my-service",
|
|
70
70
|
"projects.register": "Register project",
|
|
71
71
|
"projects.listTitle": "Projects",
|
|
72
72
|
"projects.count": ({ count }) => (count === 1 ? "1 project" : `${count} projects`),
|
|
@@ -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",
|
|
@@ -64,9 +64,9 @@ export default {
|
|
|
64
64
|
"projects.registryEyebrow": "项目登记",
|
|
65
65
|
"projects.addTitle": "新增项目",
|
|
66
66
|
"projects.name": "项目名",
|
|
67
|
-
"projects.namePlaceholder": "例如
|
|
67
|
+
"projects.namePlaceholder": "例如 my-service",
|
|
68
68
|
"projects.path": "本地路径",
|
|
69
|
-
"projects.pathPlaceholder": "/workspace/
|
|
69
|
+
"projects.pathPlaceholder": "/workspace/my-service",
|
|
70
70
|
"projects.register": "登记项目",
|
|
71
71
|
"projects.listTitle": "项目列表",
|
|
72
72
|
"projects.count": ({ count }) => `${count} 个项目`,
|
|
@@ -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": "本轮允许",
|
|
@@ -8,11 +8,35 @@ const TURN_STATUS_PRIORITY = {
|
|
|
8
8
|
failed: 3,
|
|
9
9
|
aborted: 4,
|
|
10
10
|
};
|
|
11
|
+
const MAX_COMMAND_OUTPUT_CHARS = 80 * 1024;
|
|
12
|
+
const MAX_PATCH_OUTPUT_CHARS = 48 * 1024;
|
|
13
|
+
const OUTPUT_TRUNCATION_NOTICE = "\n\n[output truncated]\n";
|
|
11
14
|
|
|
12
15
|
function createItemIndex() {
|
|
13
16
|
return new Map();
|
|
14
17
|
}
|
|
15
18
|
|
|
19
|
+
function clampOutputText(text, maxChars = MAX_COMMAND_OUTPUT_CHARS) {
|
|
20
|
+
const safeText = String(text || "");
|
|
21
|
+
if (!safeText) {
|
|
22
|
+
return "";
|
|
23
|
+
}
|
|
24
|
+
if (safeText.endsWith(OUTPUT_TRUNCATION_NOTICE)) {
|
|
25
|
+
return safeText;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const contentLimit = Math.max(0, maxChars - OUTPUT_TRUNCATION_NOTICE.length);
|
|
29
|
+
if (safeText.length <= contentLimit) {
|
|
30
|
+
return safeText;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
return `${safeText.slice(0, contentLimit)}${OUTPUT_TRUNCATION_NOTICE}`;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function appendClampedOutput(currentText, textDelta, maxChars = MAX_COMMAND_OUTPUT_CHARS) {
|
|
37
|
+
return clampOutputText(`${String(currentText || "")}${String(textDelta || "")}`, maxChars);
|
|
38
|
+
}
|
|
39
|
+
|
|
16
40
|
function nextTurnFallbackId(event) {
|
|
17
41
|
return event?.turnId || `turn:${event?.id || crypto.randomUUID?.() || Date.now()}`;
|
|
18
42
|
}
|
|
@@ -526,10 +550,18 @@ export function reduceTimeline(state, event) {
|
|
|
526
550
|
const nextStdout =
|
|
527
551
|
event.payload?.stream === "stderr"
|
|
528
552
|
? currentCommand?.stdout || ""
|
|
529
|
-
:
|
|
553
|
+
: appendClampedOutput(
|
|
554
|
+
currentCommand?.stdout,
|
|
555
|
+
event.payload?.textDelta,
|
|
556
|
+
MAX_COMMAND_OUTPUT_CHARS,
|
|
557
|
+
);
|
|
530
558
|
const nextStderr =
|
|
531
559
|
event.payload?.stream === "stderr"
|
|
532
|
-
?
|
|
560
|
+
? appendClampedOutput(
|
|
561
|
+
currentCommand?.stderr,
|
|
562
|
+
event.payload?.textDelta,
|
|
563
|
+
MAX_COMMAND_OUTPUT_CHARS,
|
|
564
|
+
)
|
|
533
565
|
: currentCommand?.stderr || "";
|
|
534
566
|
upsertCommand(state, event, turnId, {
|
|
535
567
|
status: currentCommand?.status === "awaiting_approval" ? "awaiting_approval" : "running",
|
|
@@ -552,14 +584,22 @@ export function reduceTimeline(state, event) {
|
|
|
552
584
|
status: completedStatus,
|
|
553
585
|
command: event.payload?.command || state.commandsByCallId[event.callId]?.command || "",
|
|
554
586
|
cwd: event.payload?.cwd || state.commandsByCallId[event.callId]?.cwd || null,
|
|
555
|
-
stdout:
|
|
556
|
-
|
|
557
|
-
|
|
587
|
+
stdout: clampOutputText(
|
|
588
|
+
event.payload?.stdout || state.commandsByCallId[event.callId]?.stdout || "",
|
|
589
|
+
MAX_COMMAND_OUTPUT_CHARS,
|
|
590
|
+
),
|
|
591
|
+
stderr: clampOutputText(
|
|
592
|
+
event.payload?.stderr || state.commandsByCallId[event.callId]?.stderr || "",
|
|
593
|
+
MAX_COMMAND_OUTPUT_CHARS,
|
|
594
|
+
),
|
|
595
|
+
output: clampOutputText(
|
|
558
596
|
event.payload?.aggregatedOutput ||
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
597
|
+
event.payload?.formattedOutput ||
|
|
598
|
+
event.payload?.output ||
|
|
599
|
+
state.commandsByCallId[event.callId]?.output ||
|
|
600
|
+
"",
|
|
601
|
+
MAX_COMMAND_OUTPUT_CHARS,
|
|
602
|
+
),
|
|
563
603
|
exitCode:
|
|
564
604
|
event.payload?.exitCode ?? state.commandsByCallId[event.callId]?.exitCode ?? null,
|
|
565
605
|
duration: event.payload?.duration || state.commandsByCallId[event.callId]?.duration || null,
|
|
@@ -583,7 +623,11 @@ export function reduceTimeline(state, event) {
|
|
|
583
623
|
const currentPatch = state.patchesByCallId[patchId];
|
|
584
624
|
upsertPatch(state, event, turnId, {
|
|
585
625
|
status: currentPatch?.status || "running",
|
|
586
|
-
output:
|
|
626
|
+
output: appendClampedOutput(
|
|
627
|
+
currentPatch?.output,
|
|
628
|
+
event.payload?.textDelta,
|
|
629
|
+
MAX_PATCH_OUTPUT_CHARS,
|
|
630
|
+
),
|
|
587
631
|
outputStatus: "streaming",
|
|
588
632
|
});
|
|
589
633
|
setTurnStatus(turn, "running");
|
|
@@ -598,9 +642,18 @@ export function reduceTimeline(state, event) {
|
|
|
598
642
|
status: patchStatus,
|
|
599
643
|
patchText:
|
|
600
644
|
event.payload?.patchText || state.patchesByCallId[event.callId]?.patchText || "",
|
|
601
|
-
output:
|
|
602
|
-
|
|
603
|
-
|
|
645
|
+
output: clampOutputText(
|
|
646
|
+
event.payload?.output || state.patchesByCallId[event.callId]?.output || "",
|
|
647
|
+
MAX_PATCH_OUTPUT_CHARS,
|
|
648
|
+
),
|
|
649
|
+
stdout: clampOutputText(
|
|
650
|
+
event.payload?.stdout || state.patchesByCallId[event.callId]?.stdout || "",
|
|
651
|
+
MAX_PATCH_OUTPUT_CHARS,
|
|
652
|
+
),
|
|
653
|
+
stderr: clampOutputText(
|
|
654
|
+
event.payload?.stderr || state.patchesByCallId[event.callId]?.stderr || "",
|
|
655
|
+
MAX_PATCH_OUTPUT_CHARS,
|
|
656
|
+
),
|
|
604
657
|
changes: event.payload?.changes || state.patchesByCallId[event.callId]?.changes || {},
|
|
605
658
|
success:
|
|
606
659
|
event.payload?.success ?? state.patchesByCallId[event.callId]?.success ?? null,
|