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/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.resumable !== false &&
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: true,
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) => item?.status === "pending" && isApprovalSuppressed(session?.sessionId, item?.requestId),
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
- nextRawEvents.forEach((rawEvent) => {
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
- const payload = await getSessionEvents(sessionId, {
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) ? 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
- state.workspace.activeSessionId = sessionId;
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(sessionId),
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(sessionId);
3150
+ bindWorkspaceSidebarControls(normalizedSessionId);
3064
3151
  }
3065
3152
 
3066
3153
  try {
3067
- await syncImportedSession(sessionId).catch(() => null);
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(sessionId),
3071
- loadInitialSessionEvents(sessionId),
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(sessionId);
3128
- state.detail.codexStatus = await getCodexStatus({
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
- attachSessionSocket(sessionId);
3143
- void catchUpSessionEvents(sessionId, state.detail.cursor)
3239
+ if (isStaleDetailLoad()) {
3240
+ return;
3241
+ }
3242
+
3243
+ attachSessionSocket(normalizedSessionId);
3244
+ void catchUpSessionEvents(normalizedSessionId, state.detail.cursor)
3144
3245
  .then(() => {
3145
- scheduleSessionDetailRender();
3246
+ if (!isStaleDetailLoad()) {
3247
+ scheduleSessionDetailRender();
3248
+ }
3146
3249
  })
3147
3250
  .catch(() => null);
3148
- scheduleImportedSessionSync(sessionId);
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(sessionId),
3262
+ sidebarHtml: renderWorkspaceSidebar(normalizedSessionId),
3156
3263
  mainHtml: errorCard(messageOf(error)),
3157
3264
  });
3158
3265
  syncWorkspaceShellState();
3159
3266
  bindWorkspaceCreateDialogControls();
3160
3267
  bindWorkspaceImportDialogControls();
3161
- bindWorkspaceSidebarControls(sessionId);
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
- state.ws = connectSessionSocket(sessionId, {
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(sessionId);
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(state.detail.session.sessionId, event.payload);
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
- <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>
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 || banner.getAttribute("data-approval-resumable") === "false") {
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
- state.detail.pendingApproval = previousPendingApproval;
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
  }
@@ -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. easygo-service",
67
+ "projects.namePlaceholder": "e.g. my-service",
68
68
  "projects.path": "Local path",
69
- "projects.pathPlaceholder": "/workspace/easygo-service",
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": "例如 easygo-service",
67
+ "projects.namePlaceholder": "例如 my-service",
68
68
  "projects.path": "本地路径",
69
- "projects.pathPlaceholder": "/workspace/easygo-service",
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
- : appendDeltaText(currentCommand?.stdout, event.payload?.textDelta);
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
- ? appendDeltaText(currentCommand?.stderr, event.payload?.textDelta)
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: event.payload?.stdout || state.commandsByCallId[event.callId]?.stdout || "",
556
- stderr: event.payload?.stderr || state.commandsByCallId[event.callId]?.stderr || "",
557
- output:
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
- event.payload?.formattedOutput ||
560
- event.payload?.output ||
561
- state.commandsByCallId[event.callId]?.output ||
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: appendDeltaText(currentPatch?.output, event.payload?.textDelta),
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: event.payload?.output || state.patchesByCallId[event.callId]?.output || "",
602
- stdout: event.payload?.stdout || state.patchesByCallId[event.callId]?.stdout || "",
603
- stderr: event.payload?.stderr || state.patchesByCallId[event.callId]?.stderr || "",
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,