Skip to content
Merged
Show file tree
Hide file tree
Changes from 14 commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
0c38ce0
feat(core): add willCleanUp.ts module to manage cleanups before unload
andy-ish Aug 1, 2026
e0db382
feat: apply WillCleanUp to reduce code repeat in src/pages/content/in…
andy-ish Aug 1, 2026
f0e982c
chore: remove redundant console.log statement
andy-ish Aug 1, 2026
b330786
fix: willCleanUp.ts can safely handle cleanup functions that throws a…
andy-ish Aug 1, 2026
f28d57c
Merge branch 'Nagi-ovo:main' into refactor/entrypoint
andy-ish Aug 2, 2026
ab1a9a4
chore: rename WillCleanUp and its related identifiers to make the cod…
andy-ish Aug 4, 2026
18fbd08
Merge branch 'Nagi-ovo:main' into refactor/entrypoint
andy-ish Aug 4, 2026
e7f013d
feat: add cleanupManager sequence support and returning cleanup funct…
andy-ish Aug 4, 2026
f8e9ad9
feat: apply returning function as-is feature to code entrypoint
andy-ish Aug 4, 2026
51a8f44
Merge branch 'refactor/entrypoint' of https://github.com/andy-ish/voy…
andy-ish Aug 4, 2026
7b621e2
feat(cleanup): apply cleanup sequence feature to code entrypoint
andy-ish Aug 4, 2026
b951e9e
feat(cleanup): add withdraw cleanup functions feature and its tests
andy-ish Aug 4, 2026
8215069
feat(cleanup): apply the withdrawal cleanup functions feature
andy-ish Aug 4, 2026
f9f379e
Merge branch 'Nagi-ovo:main' into refactor/entrypoint
andy-ish Aug 5, 2026
4337dfd
Merge branch 'Nagi-ovo:main' into refactor/entrypoint
andy-ish Aug 6, 2026
057a71c
fix: resolve cleanupManager incorrectly ignoring cleanup functions th…
andy-ish Aug 6, 2026
b17c614
test(cleanup): lock legacy cleanup order
Nagi-ovo Aug 7, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 24 additions & 0 deletions src/core/types/cleanupPositions.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
export enum CleanupPositions {
RemoveUnhandledRejectionEventListener,
RemoveErrorEventListener,
StopWatermarkRemover,
DestroyFolderManagerInstance,
DestroyPromptManagerInstance,
DestroySlashPromptFeatureInstance,
CleanupQuoteReply,
CleanupInputVimMode,
CleanupSendBehavior,
CleanupDraftSave,
CleanupFork,
CleanupGemsSidebar,
CleanupResponseCompleteNotification,
CleanupEdgeFinalVersionNotice,
CleanupPluginHost,
CleanupBrandTheme,
CleanupRemoteAnnouncements,
CleanupStorageQuotaWarning,
CleanupAccountContextBridge,
CleanupCodeBlockCollapse,
CleanupUsageStatus,
RemoveStorageOnChangedListener,
}
Comment on lines +1 to +24

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

upstream="$(git rev-parse --abbrev-ref --symbolic-full-name '@{upstream}' 2>/dev/null || true)"
if [[ -n "$upstream" ]]; then
  base="$(git merge-base HEAD "$upstream")"
elif git rev-parse --verify HEAD^ >/dev/null 2>&1; then
  base="HEAD^"
else
  echo "No comparison base is available."
  exit 0
fi

git diff --unified=100 "$base"...HEAD -- \
  src/pages/content/index.tsx \
  src/core/types/cleanupPositions.ts

Repository: Nagi-ovo/voyager

Length of output: 154


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '%s\n' '--- repository state ---'
git status --short
printf '%s\n' '--- recent commits ---'
git log --oneline -8
printf '%s\n' '--- cleanup-related files ---'
git ls-files | rg '(^|/)(cleanup|Cleanup)|src/pages/content/index\.tsx$' || true
printf '%s\n' '--- cleanup symbol references ---'
rg -n --hidden --glob '!node_modules' --glob '!dist' \
  'CleanupPositions|executeCleanups|beforeunload|RemoveUnhandledRejectionEventListener|RemoveErrorEventListener' .

Repository: Nagi-ovo/voyager

Length of output: 11255


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '%s\n' '--- cleanup-related commit summaries ---'
git show --stat --oneline --no-renames 7b621e2f
git show --stat --oneline --no-renames b951e9e2
git show --stat --oneline --no-renames 82150698

printf '%s\n' '--- entrypoint changes across cleanup commits ---'
git show --format=fuller --no-ext-diff --unified=12 7b621e2f -- src/pages/content/index.tsx src/core/types/cleanupPositions.ts src/core/utils/cleanupManager.ts
git show --format=fuller --no-ext-diff --unified=12 b951e9e2 -- src/pages/content/index.tsx src/core/types/cleanupPositions.ts src/core/utils/cleanupManager.ts
git show --format=fuller --no-ext-diff --unified=12 82150698 -- src/pages/content/index.tsx src/core/types/cleanupPositions.ts src/core/utils/cleanupManager.ts

Repository: Nagi-ovo/voyager

Length of output: 27105


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '%s\n' '--- cleanup manager implementation ---'
cat -n src/core/utils/cleanupManager.ts

printf '%s\n' '--- previous entrypoint teardown ---'
git show 7b621e2f^:src/pages/content/index.tsx | sed -n '620,760p'

printf '%s\n' '--- current entrypoint teardown and registrations ---'
sed -n '540,750p' src/pages/content/index.tsx

Repository: Nagi-ovo/voyager

Length of output: 12966


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '%s\n' '--- previous cleanup registrations in source order ---'
git show 7b621e2f^:src/pages/content/index.tsx |
  nl -ba |
  rg 'registerCleanupFunction|registerCleanupFunctionAndReturnIt'

printf '%s\n' '--- current cleanup registrations in source order ---'
nl -ba src/pages/content/index.tsx |
  rg 'registerCleanupFunction|registerCleanupFunctionAndReturnIt'

printf '%s\n' '--- previous cleanup registrations with context ---'
git show 7b621e2f^:src/pages/content/index.tsx |
  nl -ba |
  rg -C 3 'registerCleanupFunction|registerCleanupFunctionAndReturnIt'

Repository: Nagi-ovo/voyager

Length of output: 245


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '%s\n' '--- previous cleanup registrations in source order ---'
git show 7b621e2f^:src/pages/content/index.tsx |
  grep -nE 'registerCleanupFunction|registerCleanupFunctionAndReturnIt' || true

printf '%s\n' '--- current cleanup registrations in source order ---'
grep -nE 'registerCleanupFunction|registerCleanupFunctionAndReturnIt' src/pages/content/index.tsx || true

printf '%s\n' '--- previous cleanup registrations with context ---'
git show 7b621e2f^:src/pages/content/index.tsx |
  grep -n -E -B3 -A3 'registerCleanupFunction|registerCleanupFunctionAndReturnIt' || true

Repository: Nagi-ovo/voyager

Length of output: 11873


🏁 Script executed:

#!/bin/bash
set -euo pipefail

python3 - <<'PY'
import re
from pathlib import Path

enum_text = Path("src/core/types/cleanupPositions.ts").read_text()
names = re.findall(r"^\s{2}([A-Za-z][A-Za-z0-9_]*),?$", enum_text, re.M)
positions = {name: index for index, name in enumerate(names)}

legacy_gemini_order = [
    "CleanupAccountContextBridge",
    "CleanupPluginHost",
    "CleanupBrandTheme",
    "RemoveUnhandledRejectionEventListener",
    "RemoveErrorEventListener",
    "CleanupRemoteAnnouncements",
    "CleanupStorageQuotaWarning",
    "RemoveStorageOnChangedListener",
    "DestroySlashPromptFeatureInstance",
    "CleanupEdgeFinalVersionNotice",
    "DestroyFolderManagerInstance",
    "CleanupInputVimMode",
    "CleanupSendBehavior",
    "CleanupQuoteReply",
    "StopWatermarkRemover",
    "CleanupResponseCompleteNotification",
    "CleanupDraftSave",
    "CleanupGemsSidebar",
    "CleanupUsageStatus",
    "CleanupCodeBlockCollapse",
    "CleanupFork",
    "DestroyPromptManagerInstance",
]

actual = [positions[name] for name in legacy_gemini_order]
inversions = [
    (legacy_gemini_order[i], legacy_gemini_order[j])
    for i in range(len(actual))
    for j in range(i + 1, len(actual))
    if actual[i] > actual[j]
]

print("Enum positions:")
for name in legacy_gemini_order:
    print(f"{name}: {positions[name]}")
print(f"Legacy order preserved: {not inversions}")
print("First ordering differences:")
for left, right in inversions[:12]:
    print(f"{left} must run before {right}, but the enum runs them in the opposite order")
PY

Repository: Nagi-ovo/voyager

Length of output: 2231


Preserve the legacy cleanup order. CleanupManager.executeCleanups() previously used registration order because every cleanup had position -1. The enum reverses existing ordering; for example, CleanupAccountContextBridge was registered before CleanupPluginHost, but positions 18 and 14 execute them in the opposite order. Reorder the enum or add tests for the intended dependency order.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/core/types/cleanupPositions.ts` around lines 1 - 24, Update
CleanupPositions and its consumers so CleanupManager.executeCleanups() preserves
the legacy registration order, particularly ensuring CleanupAccountContextBridge
runs before CleanupPluginHost and all other existing dependencies retain their
prior sequence. Reorder the enum values to match that order, or add coverage
that verifies the intended cleanup dependency ordering.

119 changes: 119 additions & 0 deletions src/core/utils/__tests__/cleanupManager.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
import { beforeEach, describe, expect, it, vi } from 'vitest';

import { CleanupManager } from '@/core/utils/cleanupManager';

enum Sequence {
First,
Second,
Third,
}

describe('willCleanUp tests module', () => {
let cleanupManager: CleanupManager;

beforeEach(() => {
cleanupManager = new CleanupManager();
});

it('can store registered cleanup functions', () => {
const function1 = () => {};
const function2 = () => {};

cleanupManager.registerCleanupFunction(function1);
cleanupManager.registerCleanupFunction(function2);
cleanupManager.registerCleanupFunction(function2); // won't store duplicate functions

expect(cleanupManager.list()).toEqual([
{
pos: -1,
func: function1,
},
{
pos: -1,
func: function2,
},
]);
});

it('can return registered functions as-is', () => {
const function1 = () => {};

expect(cleanupManager.registerCleanupFunctionAndReturnIt(function1)).toBe(function1);
});

it('can execute registered cleanup functions at correct time', () => {
const function1 = vi.fn();

cleanupManager.registerCleanupFunction(function1);

expect(function1).not.toHaveBeenCalled();

cleanupManager.executeCleanups();

expect(function1).toHaveBeenCalled();
expect(function1).toHaveBeenCalledTimes(1);

cleanupManager.executeCleanups(); // no duplicate call

expect(function1).toHaveBeenCalledTimes(1);
});

it('can release stored cleanup functions at correct time', () => {
const function1 = () => {};

cleanupManager.registerCleanupFunction(function1);

expect(cleanupManager.list()).not.toEqual([]);

cleanupManager.executeCleanups();

expect(cleanupManager.list()).toEqual([]);
});

it('can safely handle cleanup functions that throws an error', () => {
const function1 = () => {
throw Error();
};
const function2 = vi.fn();

cleanupManager.registerCleanupFunction(function1);
cleanupManager.registerCleanupFunction(function2);

expect(() => cleanupManager.executeCleanups()).toThrow();

expect(function2).toHaveBeenCalled();
expect(function2).toHaveBeenCalledTimes(1);
expect(cleanupManager.list()).toEqual([]);
});

it('can call functions in correct sequence', () => {
const function1 = vi.fn();
const function2 = vi.fn();
const function3 = vi.fn();

cleanupManager.registerCleanupFunction(function3, Sequence.Third);
cleanupManager.registerCleanupFunction(function2, Sequence.Second);
cleanupManager.registerCleanupFunction(function1, Sequence.First);

cleanupManager.executeCleanups();

expect(function2).toHaveBeenCalledAfter(function1);
expect(function3).toHaveBeenCalledAfter(function2);
});

it('can withdraw functions by position number', () => {
const function1 = vi.fn();
const function2 = vi.fn();
const function3 = vi.fn();

cleanupManager.registerCleanupFunction(function3, Sequence.Third);
cleanupManager.registerCleanupFunction(function2, Sequence.Second);
cleanupManager.registerCleanupFunction(function1, Sequence.First);

cleanupManager.withdrawCleanupFunctionsByPositionNumber(Sequence.Second);

expect(cleanupManager.list().some((cleanups) => cleanups.func === function1)).toBe(true);
expect(cleanupManager.list().some((cleanups) => cleanups.func === function2)).toBe(false);
expect(cleanupManager.list().some((cleanups) => cleanups.func === function3)).toBe(true);
});
});
78 changes: 78 additions & 0 deletions src/core/utils/cleanupManager.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
/**
* A class that manages and executes cleanup functions in code entrypoint.
*/
export class CleanupManager {
private cleanups: Array<Cleanup> = [];

constructor() {}

/**
* Register a cleanup function waited to be called.
* @param func A function that does cleanup operation when called.
* @param pos A number (preferably defined by Enum) that indicates the position of the
* cleanup function. The lower the number, the earlier the function would be called.
*/
registerCleanupFunction(func: () => void, pos: number = -1): void {
if (this.cleanups.some((cleanup) => cleanup.func === func)) return;
this.cleanups.push({
pos: pos,
func: func,
});
}

/**
* Register a cleanup function, and return the function as-is.
* @param func
* @param pos
*/
registerCleanupFunctionAndReturnIt(func: () => void, pos: number = -1): () => void {
this.registerCleanupFunction(func, pos);
return func;
}

/**
* Remove any cleanup functions associated with the given position number.
* @param pos Position number for functions which will be removed.
*/
withdrawCleanupFunctionsByPositionNumber(pos: number): void {
this.cleanups = this.cleanups.filter((cleanup) => cleanup.pos != pos);
}

/**
* [debug] return a readonly list containing stored cleanup functions.
*/
list(): Array<Cleanup> {
return [...this.cleanups] as const;
Comment on lines +44 to +45

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

rg -n -C 2 \
  'list\(\): Array<Cleanup>|return \[\.\.\.this\.cleanups\] as const' \
  src/core/utils/cleanupManager.ts

# Then run the repository's normal TypeScript type-check job locally or in CI.
# Expect no readonly-array assignment error for src/core/utils/cleanupManager.ts.

Repository: Nagi-ovo/voyager

Length of output: 323


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '%s\n' '--- cleanupManager.ts ---'
cat -n src/core/utils/cleanupManager.ts | sed -n '1,90p'

printf '%s\n' '--- project type-check configuration ---'
for f in package.json tsconfig.json; do
  if [ -f "$f" ]; then
    printf '%s\n' "--- $f ---"
    cat -n "$f" | sed -n '1,220p'
  fi
done

printf '%s\n' '--- TypeScript tooling availability ---'
command -v tsc || true
tsc --version 2>/dev/null || true

Repository: Nagi-ovo/voyager

Length of output: 9253


🏁 Script executed:

#!/bin/bash
set -uo pipefail

printf '%s\n' '--- repository type-check ---'
tsc --noEmit --pretty false 2>&1
status=$?
printf 'tsc_exit=%s\n' "$status"

printf '%s\n' '--- isolated return-type check ---'
tmp="$(mktemp --suffix=.ts)"
trap 'rm -f "$tmp"' EXIT
cat >"$tmp" <<'TS'
interface Cleanup {
  pos: number;
  func: () => void;
}
class CleanupManager {
  private cleanups: Array<Cleanup> = [];
  list(): Array<Cleanup> {
    return [...this.cleanups] as const;
  }
}
TS
tsc --noEmit --strict --target esnext --module esnext --pretty false "$tmp" 2>&1
printf 'isolated_tsc_exit=%s\n' "$?"

Repository: Nagi-ovo/voyager

Length of output: 2088


🏁 Script executed:

#!/bin/bash
set -uo pipefail

tmp="$(mktemp --suffix=.ts)"
trap 'rm -f "$tmp"' EXIT
cat >"$tmp" <<'TS'
interface Cleanup {
  pos: number;
  func: () => void;
}
class CleanupManager {
  private cleanups: Array<Cleanup> = [];
  list(): Array<Cleanup> {
    return [...this.cleanups] as const;
  }
}
TS

tsc --ignoreConfig --noEmit --strict --target esnext --module esnext --pretty false "$tmp" 2>&1
printf 'isolated_tsc_exit=%s\n' "$?"

Repository: Nagi-ovo/voyager

Length of output: 174


🏁 Script executed:

#!/bin/bash
set -uo pipefail

tmp="$(mktemp --suffix=.ts)"
trap 'rm -f "$tmp"' EXIT
cat >"$tmp" <<'TS'
const direct: number[] = [1, 2] as const;

const source: number[] = [];
const spread: number[] = [...source] as const;

interface Cleanup {
  pos: number;
  func: () => void;
}
class CleanupManager {
  private cleanups: Array<Cleanup> = [];
  list(): Array<Cleanup> {
    return [...this.cleanups] as const;
  }
}
TS

tsc --ignoreConfig --noEmit --strict --target esnext --module esnext --pretty false "$tmp" 2>&1
printf 'probe_tsc_exit=%s\n' "$?"

Repository: Nagi-ovo/voyager

Length of output: 171


🏁 Script executed:

#!/bin/bash
set -uo pipefail

printf '%s\n' '--- available runtimes and local dependencies ---'
command -v bun || true
bun --version 2>/dev/null || true
command -v node || true
node --version 2>/dev/null || true
if [ -d node_modules/typescript ]; then
  node -p "require('./node_modules/typescript/package.json').version" 2>/dev/null || true
else
  printf '%s\n' 'node_modules/typescript: absent'
fi

printf '%s\n' '--- lockfile TypeScript entries ---'
for f in bun.lock bun.lockb package-lock.json yarn.lock pnpm-lock.yaml; do
  if [ -f "$f" ]; then
    printf '%s\n' "--- $f ---"
    rg -n -C 2 'typescript@|typescript:' "$f" | head -80
  fi
done

Repository: Nagi-ovo/voyager

Length of output: 829


🌐 Web query:

TypeScript 5.8 readonly tuple as const assignable to mutable array TS4104

💡 Result:

The error TS4104 occurs in TypeScript when you attempt to assign a readonly array or tuple (often created via as const) to a location that expects a mutable array [1][2][3]. This error exists because readonly types are not assignable to mutable types [4][5]. A mutable type (e.g., T[]) implies that the array can be modified (elements added, removed, or reordered), whereas a readonly type (e.g., readonly T[]) guarantees that it will not be [4]. Allowing this assignment would be unsafe because the consuming code might attempt to mutate the array, violating the readonly contract [4]. To resolve this, you can use one of the following approaches depending on your requirements: 1. Spread the array into a fresh literal: This creates a new, mutable copy of the array elements [6]. const mutableArray = [...myReadonlyTuple]; 2. Cast the value to a mutable type: If you are certain the consuming code will not mutate the array, or if you simply need to satisfy the type checker, you can cast it [7][5]. const mutableArray = myReadonlyTuple as any[]; // Or more safely: const mutableArray = myReadonlyTuple as T[]; 3. Update the consuming type: If you own the code consuming the array, change the type definition to accept a readonly array, which is generally considered better practice for functions that do not intend to modify their inputs [4][7]. function process(data: readonly T[]) {... } The TS4104 error was more prominent in earlier versions of TypeScript (around 5.0-5.1) following stricter checks on readonly-to-mutable assignments [8][9][10]. While the compiler does not automatically "strip" readonly for you [11], using a spread operator is the idiomatic way to produce a mutable array from a readonly one [6].

Citations:


Remove the incompatible as const assertion.

list() returns a readonly array, but its return type is mutable Array<Cleanup>. TypeScript 5.9.3 rejects this assignment.

Proposed fix
   list(): Array<Cleanup> {
-    return [...this.cleanups] as const;
+    return [...this.cleanups];
   }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
list(): Array<Cleanup> {
return [...this.cleanups] as const;
list(): Array<Cleanup> {
return [...this.cleanups];
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/core/utils/cleanupManager.ts` around lines 44 - 45, Update the list()
method in the cleanup manager to remove the incompatible as const assertion and
return the copied cleanups as the declared mutable Array<Cleanup> type.

}

/**
* Call all functions registered by `registerCleanupFunction` functions,
* and clear their references.
*
* If any cleanup functions throws an error, other functions will execute normally.
* Then, the last recorded error will be re-thrown.
*/
executeCleanups(): void {
let error: unknown = null;

this.cleanups
.sort((a, b) => {
return a.pos - b.pos;
})
.forEach((it) => {
try {
it.func();
} catch (e) {
error = e;
}
});
this.cleanups = [];

if (error) throw error;
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated
}
}

interface Cleanup {
pos: number;
func: () => void;
}
Loading
Loading