Conversation search still surfaced hidden data after archiving because it only filtered out archived
session rows. Active session rows that belonged to archived projects were still indexed, which broke
the expectation that archived work disappears from normal search until it is restored.
This change makes search follow the visible workspace model by skipping any session whose owning
project is archived before ripgrep and transcript parsing begin. The archive-state lookup is cached
per project path so the behavior is corrected without adding repeated database work during a scan.
Users previously had an all-or-nothing choice for completed sessions: either keep them in the active
sidebar or permanently delete them. That made long-lived usage brittle because valuable history
stayed in the way unless users destroyed it. This change introduces archiving as a first-class
lifecycle so completed work can be hidden without losing transcript history, workspace context,
or restoreability.
The backend now persists session archive state and excludes archived rows from active session
queries by default. Dedicated archive queries and routes make archived sessions and archived
workspaces addressable on their own, which is necessary once hidden data can no longer be rebuilt
from the active project list. Hard-delete behavior still cleans up transcript files so destructive
deletes remain truly destructive.
The frontend now mirrors that lifecycle in the sidebar. Delete flows distinguish between archive
and permanent delete, archived sessions can be restored, archived workspaces appear beside
standalone archived sessions, and archived project sessions open with the correct workspace context
instead of routing to a session URL that leaves the main view empty. Follow-up archive UI polish
keeps the status affordances explicit without competing with workspace names.
Claude writes slash-command metadata, local stdout, and compaction summaries
into the same JSONL stream as normal chat messages. The existing
normalization path treated those rows as internal content and dropped them
entirely.
That made the web UI diverge from the CLI transcript and removed important
context. Commands like /compact appeared to have never happened, the stdout
status line disappeared, and the continuation summary after compaction was
filtered out even though it best describes the post-boundary session state.
This change keeps the distinction between truly internal transcript rows and
user-visible local command artifacts. Command wrapper tags are parsed into
structured metadata without exposing the raw tags, local command stdout is
remapped to assistant text, and compact summaries are preserved as
assistant-authored content instead of being mislabeled as user input.
Search and session-summary parsing are updated for the same reason. If
history normalization preserved these rows but search still ignored them,
rendered conversation state and searchable conversation state would continue
to disagree, and session summaries would fall back to stale user text
instead of Claude's actual compaction summary.
The shared message and store typings are extended so this metadata survives
the full backend-to-frontend pipeline. That avoids reconstructing meaning
later and keeps the transcript faithful to Claude's persisted history while
still hiding genuinely internal control content.
Why:
- Cursor normalization emits internal tool_result items so tool outputs can attach to tool cards.
- The UI does not render tool_result as standalone rows.
- Pagination previously mixed datasets: total excluded tool_result, but page slicing and
hasMore used the unfiltered collection.
- That mismatch caused offset and limit drift versus what users actually see.
- Tool normalization also renamed ApplyPatch to Edit before input normalization.
- That hid the original tool identity from normalizeCursorToolInput and could drop
patch-specific shaping.
What changed:
- Added one pagination source of truth: renderableMessages filters out tool_result.
- total, page slicing, and hasMore now all use renderableMessages.
- Unlimited-history responses now return renderableMessages for consistent semantics.
- normalizeCursorToolInput now receives rawToolName first.
- The user-facing rename from ApplyPatch to Edit is still preserved in toolName.
Impact:
- Offset and limit now map to visible Cursor rows.
- hasMore reflects remaining renderable history.
- ApplyPatch payloads keep patch-aware normalization while preserving UI naming.
Why:
- Gemini exit-code 41 auth guidance was defined in two places with slightly different wording, which creates drift and inconsistent UX when auth fails.
- Gemini env bootstrap only short-circuited when *all* auth-related vars were present; that forced unnecessary user-level env file reads even when a valid primary credential already existed.
- Codex session registration only populated the active-session map when an id was unseen; resumed sessions could retain stale thread/abort/status objects, leading to incorrect lifecycle behavior.
- Debug console logs in runtime request paths added noise without operational value.
What changed:
- Removed exit-code 41 from mapGeminiExitCodeToMessage and kept a single authoritative 41 message in the close-handler auth branch.
- Aligned 41 remediation phrasing to consistently reference a valid GEMINI_API_KEY.
- Updated uildGeminiProcessEnv to return early when any primary auth signal is already present (GEMINI_API_KEY, GOOGLE_API_KEY, or GOOGLE_APPLICATION_CREDENTIALS).
- Changed Codex
egisterSession to always overwrite session state for valid ids so resumed runs replace stale entries.
- Removed unnecessary console.log debug statements in touched runtime paths, including the chat submit debug log.
Impact:
- More predictable Gemini authentication error messaging.
- Avoids needless env-file fallback when credentials are already available.
- Prevents stale Codex session controllers/status from leaking across resumes.
- Cleaner logs with no behavior change to error/warn reporting.
Cursor sessions were still showing inflated totals after the earlier total-count
work, producing UX mismatches like "Showing 16 of 31" while only ~16 visible
chat rows existed.
Why this happened:
- Cursor history normalization emits both `tool_use` and `tool_result` entries.
- The UI intentionally does not render `tool_result` as its own message bubble;
it attaches tool results onto the related `tool_use` card.
- Total counting that includes `tool_result` therefore measures transport
artifacts, not user-visible conversation rows.
- In practice, this makes totals look wrong and undermines trust in pagination
status and session message counts.
Why this change:
- `total` is a user-facing semantic value and must represent what the user can
actually see in the timeline.
- Cursor should follow the same rendered-message counting rule as other
providers to keep behavior predictable across providers.
- Avoiding hidden/internal row counts in totals prevents confusion in
"showing X of Y" indicators and load-more expectations.
What was changed:
- Replaced implicit/raw-style counting with explicit provider-side total
tracking in Cursor history fetch.
- Total tracker increments only for processed messages that map to rendered chat
rows, excluding standalone `tool_result` entries.
- Pagination windowing remains based on full normalized history payload to avoid
changing ordering/loading mechanics; only displayed `total` semantics were
corrected.
Result:
- Cursor `total` now reflects rendered chat rows rather than internal normalized
event count.
- Session counters are now aligned with what users perceive in the UI.
The session `total` value was diverging from the number of rendered chat rows,
which created confusing UI states (for example: "showing 12 of 21" when only 12
messages exist visually).
Why this was happening:
- Providers were counting transport/normalized records, not renderable chat rows.
- `tool_result` records are normalized and needed for tool wiring, but the UI
does not render them as standalone bubbles; they are attached to their
corresponding `tool_use`.
- As a result, totals were inflated by implementation details in the history
format rather than user-visible conversation content.
Why this change:
- `total` is a user-facing metric and should represent frontend-visible messages.
- We need provider behavior to be consistent (Codex/Claude/Cursor/Gemini) so
pagination labels, load-more affordances, and session stats match user
expectations.
- Correctness here is UX-critical: users interpret `total` as conversation
message count, not internal event count.
Implementation approach:
- Replace post-hoc generic counting logic with explicit per-provider total
trackers in each `fetchHistory` flow.
- Increment totals during provider message processing so counting rules are
owned by the provider pipeline itself.
- Exclude `tool_result` from the total tracker since it is rendered as attached
tool output, not as a standalone chat message.
Behavioral impact:
- `total` now aligns with rendered chat rows.
- Pagination mechanics remain based on normalized history payloads, so loading
behavior is unchanged while user-visible totals are corrected.
- Tool result attachment behavior is preserved.
Touched providers:
- codex-sessions.provider.ts
- claude-sessions.provider.ts
- cursor-sessions.provider.ts
- gemini-sessions.provider.ts
Why:
- Gemini does not expose a new session id synchronously.
It emits the canonical id in the init stream event.
- Creating temporary ids in web mode introduced identity drift,
extra mapping logic, and harder resume/debug behavior.
- Headless server runs often miss shell-inherited auth vars,
while users configure Gemini through user-level env files.
- Gemini mirrors session artifacts across folders,
which caused duplicate sync events and duplicate session rows.
What changed:
- Removed temporary Gemini ids for new sessions.
- New Gemini sessions are now created only after init provides session_id.
- Persisted cliSessionId from the discovered canonical id,
keeping one identifier across stream, storage, and resume.
- Built Gemini spawn env from process env plus user-level fallback files:
~/.gemini/.env then ~/.env, honoring GEMINI_CLI_HOME.
- Added --skip-trust for headless runs,
because web flows cannot answer interactive trust prompts.
- Improved terminal error mapping and rejection reasons,
especially for auth exit code 41 with actionable context.
- Limited Gemini synchronization to tmp JSONL chat artifacts,
and disabled duplicate watcher/index paths that mirror the same sessions.
- Added gemini-2.5-flash-lite to shared model constants.
Result:
- Gemini session identity is canonical and provider-consistent.
- Headless auth now matches practical Gemini CLI configuration patterns.
- Duplicate Gemini session indexing is reduced at the source.
- Operators get clearer, actionable failure messages.
Sidebar session sorting and time labels currently normalize timestamps through
getUpdatedTimestamp. That helper still checked session.updated_at as a fallback,
but in the current project/session API shape the sidebar no longer receives raw DB
session rows.
Why this cleanup matters:
- Sidebar session items are built from project payload SessionSummary objects,
where lastActivity is already the canonical field.
- The backend guarantees lastActivity is populated from
updated_at ?? created_at ?? now before data reaches the client.
- Keeping a client-side updated_at fallback implies mixed payload contracts and
hides schema drift instead of surfacing it.
- Removing the fallback narrows the UI contract to one timestamp source,
which reduces ambiguity in session ordering and display-time logic.
This change intentionally keeps behavior the same while making the dependency
explicit: sidebar timestamp logic now reads only session.lastActivity.
The frontend realtime pipeline was carrying control-plane events and unused state
through message storage as if they were renderable chat content. That made the
session path noisier than necessary and increased the chance of subtle drift
between transport events and UI data.
Why this change:
- Control events like session_created, status, complete, and permission lifecycle
updates drive UI side effects, not chat transcript rendering.
- Persisting those events in the session store added avoidable churn, memory
growth, and merge work while providing no user-visible value.
- An unused streamBufferRef existed in the hot path, creating extra writes and
cognitive overhead with no read consumer.
- useChatRealtimeHandlers accepted selectedProject even though it was not used,
which widened the hook surface and dependency noise without behavior impact.
What this commit does:
- Removes the write-only streamBufferRef from ChatInterface and realtime handler
wiring.
- Removes the unused selectedProject argument from useChatRealtimeHandlers.
- Stops appending non-rendered control events to sessionStore realtime messages.
These events still execute their side effects exactly as before.
Net effect:
The Codex/Claude/Cursor/Gemini realtime path stays behaviorally equivalent for
users, but the data model now stores only message content that can actually be
rendered, reducing unnecessary state traffic in the chat runtime.
Codex session initialization assumed thread.id was immediately available at thread creation time. In practice, for new sessions the SDK discovers/emits the real thread id asynchronously via stream events (thread.started), matching Claude’s behavior. This early assumption caused the backend to create and publish session state before the canonical id existed.
Why this matters:
- Session identity is the join key for live streaming, abort routing, notifications, and history sync.
- Emitting session_created with a guessed/fallback id risks mismatched client state and stale references.
- Registering active sessions too early can make abort/lookup logic depend on non-canonical ids.
- Divergence from Claude’s flow created provider inconsistency and made cross-provider behavior harder to reason about.
What changed:
- Stop treating thread.id as authoritative at startup for new Codex sessions.
- Capture the real session id from thread.started (prefer event.thread_id, fallback event.id).
- Emit session_created only after the canonical id is discovered (new sessions only).
- Track active session state immediately only for resumed sessions (known sessionId), otherwise defer registration until id discovery.
- Thread the captured id through normalization, token budget updates, completion payloads, and failure notifications.
- Preserve writer/session wiring by setting ws.setSessionId once the id is known.
Result:
Codex now follows the same async session-id contract as Claude, removing a race around initial session identity and making session lifecycle handling deterministic across providers.
* fix: reset-state-on-new-session-click
* fix(chat): preserve continuity while session ids settle
New conversations were crossing a short but important consistency gap.
The route could already point at a newly created session id while the
projects payload had not refreshed yet, and realtime/optimistic messages
could still be keyed under a provisional id. In that window the UI could
stop reading the active session store, briefly render the conversation as
missing, and then repopulate it a moment later.
That same gap also made duplication more likely. Optimistic local user
messages could survive long enough to appear beside the persisted copy,
and finalized assistant streaming rows could sit directly next to the
server-backed assistant message with the same content before realtime state
was cleared. The result was a chat view that felt unstable exactly when a
new session was being created.
This commit makes session-id reconciliation a first-class part of the chat
flow instead of assuming every layer will agree immediately. The session
store now understands canonical session aliases and can migrate one
conversation from a provisional id to the real id without dropping its
in-memory state. The route navigation path can replace the provisional URL
entry instead of stacking it in history, and the project/session selection
logic keeps a synthetic selected session alive long enough for the sidebar
and project payloads to catch up.
The practical goal is to keep one visible conversation throughout the whole
creation lifecycle: no dead window between websocket events and project
refresh, no stale provisional URL after the real id is known, and no extra
optimistic/local bubbles when server history catches up.
* fix(cli): resolve executable path for Claude CLI on Windows
* fix(session-synchronizer): improve session name extraction for Claude and Codex
The /status slash command's response object falls back to a hardcoded
'claude-sonnet-4.5' when no session model is set in context. Two issues:
1. 'claude-sonnet-4.5' is not in CLAUDE_MODELS.OPTIONS (the dropdown), so
the displayed model can be a value the user can't actually select.
2. CLAUDE_MODELS.DEFAULT is currently "opus" — meaning a fresh session
started with no model picked actually runs on opus, not sonnet-4.5.
The /status command therefore mis-reports the running model.
Tracking the centralized DEFAULT keeps the displayed value coherent with
what claude-sdk.js actually spawns (line 208 already uses
`options.model || CLAUDE_MODELS.DEFAULT`).
Co-authored-by: Enrico Masella <enrico.masella@gmail.com>
Co-authored-by: Simos Mikelatos <simosmik@gmail.com>
* refactor: remove unused exports
* refactor: remove unused fields from project and session objects
* refactor: rename session_names table and related code to sessions for clarity and consistency
* refactor(database): move db into typescript
- Implemented githubTokensDb for managing GitHub tokens with CRUD operations.
- Created
otificationPreferencesDb to handle user notification preferences.
- Added projectsDb for project path management and related operations.
- Introduced pushSubscriptionsDb for managing browser push subscriptions.
- Developed scanStateDb to track the last scanned timestamp.
- Established sessionsDb for session management with CRUD functionalities.
- Created userDb for user management, including authentication and onboarding.
- Implemented apidKeysDb for storing and managing VAPID keys.
feat(database): define schema for new database tables
- Added SQL schema definitions for users, API keys, user credentials, notification preferences, VAPID keys, push subscriptions, projects, sessions, scan state, and app configuration.
- Included necessary indexes for performance optimization.
refactor(shared): enhance type definitions and utility functions
- Updated shared types and interfaces for improved clarity and consistency.
- Added new types for credential management and provider-specific operations.
- Refined utility functions for better error handling and message normalization.
* feat: added session indexer logic
* perf(projects): lazy-load TaskMaster metadata per selected project
Why:
- /api/projects is a hot path (initial load, sidebar refresh, websocket sync).
- Scanning .taskmaster for every project on each call added avoidable fs I/O and payload size.
- TaskMaster metadata is only needed after selecting a specific project.
- Moving it to a project-scoped endpoint makes loading cost match user intent.
- The UI now hydrates TaskMaster state on selection and keeps it across refresh events.
- This prevents status flicker/regression while still removing global scan overhead.
- Selection fetches are sequence-guarded to block stale async responses on fast switching.
- isManuallyAdded was removed from responses to keep the public project contract minimal.
- Project dumps now use incrementing snapshot files to preserve history for debugging.
What changed:
- Added GET /api/projects/:projectName/taskmaster and getProjectTaskMaster().
- Removed TaskMaster detection from bulk getProjects().
- Added api.projectTaskmaster(...) plus selection-time hydration in frontend contexts.
- Merged cached taskmaster values into refreshed project lists for continuity.
- Removed isManuallyAdded from manual project payloads.
* refactor: update import paths for database modules and remove legacy db.js and schema.js files
* refactor(projects): identify projects by DB projectId instead of folder-derived name
GET /api/projects used to scan ~/.claude/projects/ on every request, derive
each project's identity from the encoded folder name, and re-parse JSONL
files to build session lists. Using the folder-derived name as the project
identifier leaked the Claude CLI's on-disk encoding into every API route,
forced every downstream endpoint to re-resolve a real path via JSONL
'cwd' inspection, and made the project list endpoint O(projects x sessions)
on disk I/O.
This change switches the entire API surface to identify projects by the
stable primary key from the 'projects' table and drives the listing
straight from the DB:
- Add projectsDb.getProjectPathById as the canonical projectId -> path
resolver so routes no longer need to touch the filesystem to figure out
where a project lives.
- Rewrite getProjects so it reads the project list from the 'projects'
table and the per-project session list from the 'sessions' table (one
SELECT per project). No filesystem scanning happens for this endpoint
anymore, which removes the dependency on ~/.claude/projects existing,
on Cursor's MD5-hashed chat folders being discoverable, and on Codex's
JSONL history being on disk. Per the migration spec each session now
exposes 'summary' sourced from sessions.custom_name, 'messageCount' = 0
(message counting is not implemented), and sessionMeta.hasMore is
pinned to false since this endpoint doesn't drive session pagination.
- Introduce id-based wrappers (getSessionsById, renameProjectById,
deleteSessionById, deleteProjectById, getProjectTaskMasterById) so
every caller can pass projectId and resolve the real path through the
DB. renameProjectById also writes to projects.custom_project_name so
the DB-driven getProjects response reflects renames immediately; it
keeps project-config.json in sync for any legacy reader that still
consults the JSON file.
- Migrate every /api/projects/:projectName route in server/index.js,
server/routes/taskmaster.js, and server/routes/messages.js to
:projectId, and change server/routes/git.js so the 'project'
query/body parameter carries a projectId that is resolved through the
DB before any git command runs. TaskMaster WebSocket broadcasts emit
'projectId' for the same reason so the frontend can match
notifications against its current selection without another lookup.
- Delete helpers that existed only to feed the old getProjects path
(getCursorSessions, getGeminiCliSessions, getProjectTaskMaster) along
with their unused imports (better-sqlite3's Database,
applyCustomSessionNames). The legacy folder-name helpers (getSessions,
renameProject, deleteSession, deleteProject, extractProjectDirectory)
are kept as internal implementation details of the id-based wrappers
and of destructive cleanup / conversation search, but they are no
longer re-exported.
- searchConversations still walks JSONL to produce match snippets (that
data doesn't live in the DB), but it now includes the resolved
projectId in each result so the sidebar can cross-reference hits with
its already loaded project list without a second round-trip.
Frontend migration:
- Project.name is replaced by Project.projectId in src/types/app.ts, and
ProjectSession.__projectName becomes __projectId so session tagging
and sidebar state keys stay aligned with the backend identifier.
Settings continues to use SettingsProject.name for legacy consumers,
but it is populated from projectId by normalizeProjectForSettings.
- All places that previously indexed per-project state by project.name
(sidebar expanded/starred/loading/deletingProjects sets,
additionalSessions map, projectHasMoreOverrides, starredProjects
localStorage, command history and draft-input localStorage,
TaskMaster caches) now key on projectId so state survives
display-name edits and is consistent across the app.
- src/utils/api.js renames every endpoint parameter to projectId, the
unified messages endpoint takes projectId in its query string, and
useSessionStore forwards projectId on fetchFromServer / fetchMore /
refreshFromServer. Git panel, file tree, code editor, PRD editor,
plugins context, MCP server flows and TaskMaster hooks are all
updated to pass projectId.
- DEFAULT_PROJECT_FOR_EMPTY_SHELL is updated to carry a 'default'
projectId sentinel so the empty-shell placeholder still satisfies the
Project contract.
Bug fix bundled in:
- sessionsDb.setName no longer bumps updated_at when a row already
exists. Renaming is a label change, not activity, so there is no
reason for it to reset 'last activity' in the sidebar. It also no
longer relies on SQLite's CURRENT_TIMESTAMP, which stores a naive
'YYYY-MM-DD HH:MM:SS' value that JavaScript parses as local time and
caused renamed sessions to appear shifted backwards by the client's
UTC offset. When an INSERT actually happens it now writes ISO-8601
UTC with a 'Z' suffix.
- buildSessionsByProviderFromDb normalizes any legacy naive timestamps
in the sessions table to ISO-8601 UTC on the way out so rows written
before this change also render correctly on the client.
Other cleanup:
- Removed the filesystem-first project-discovery comment block at the
top of server/projects.js and replaced it with a short note that
describes the new DB-driven flow and lists the few remaining
filesystem-dependent helpers (message reads, search, destructive
delete, manual project registration).
- server/modules/providers/index.ts is added as a small barrel so the
providers module exposes a stable public surface.
Made-with: Cursor
* refactor(projects): reorganize project-related logic into dedicated modules
* refactor(projects): rename getProjects with getProjectsWithSessions
* refactor: update import path for getProjectsWithSessions to include file extension
* refactor: use updated session watcher
In addition, for projects_updated websocket response, send the sessionId instead
* refactor(websocket): move websocket logic to its own module
* refactor(sessions-watcher): remove redundant logging after session sync completion
* refactor(index.js): reorganize code structure
* refactor(index.js): fix import order
* refactor: remove unnecessary GitHub cloning logic from create-workspace endpoint
* refactor: modularize project services, and wizard create/clone flow
Restructure project creation, listing, GitHub clone progress, and TaskMaster
details behind a dedicated TypeScript module under server/modules/projects/,
and align the client wizard with a single path-based flow.
Server / routing
- Remove server/routes/projects.js and mount server/modules/projects/
projects.routes.ts at /api/projects (still behind authenticateToken).
- Drop duplicate handlers from server/index.js for GET /api/projects and
GET /api/projects/:projectId/taskmaster; those live on the new router.
- Import WORKSPACES_ROOT and validateWorkspacePath from shared utils in
index.js instead of the deleted projects route module.
Projects router (projects.routes.ts)
- GET /: list projects with sessions (existing snapshot behavior).
- POST /create-project: validate body, reject legacy workspaceType and
mixed clone fields, delegate to createProject service, return distinct
success copy when an archived path is reactivated.
- GET /clone-progress: Server-Sent Events for clone progress/complete/error;
requires authenticated user id for token resolution; wires startCloneProject.
- GET /:projectId/taskmaster: delegates to getProjectTaskMaster.
Services (new)
- project-management.service.ts: path validation, workspace directory
creation, persistence via projectsDb.createProjectPath, mapping to API
project shape; surfaces AppError for validation, conflict, and not-found
cases; optional dependency injection for tests.
- project-clone.service.ts: validates workspace, resolves GitHub auth
(stored token or inline token), runs git clone with progress callbacks,
registers project via createProject on success; sanitizes errors and
supports cancellation; injectable dependencies for tests.
- projects-has-taskmaster.service.ts: moves TaskMaster detection and
normalization out of server/projects.js; resolve-by-id and public
getProjectTaskMaster with structured AppError responses.
Persistence and shared types
- projectsDb.createProjectPath now returns CreateProjectPathResult
(created | reactivated_archived | active_conflict) using INSERT … ON
CONFLICT with selective update when the row is archived; normalizes
display name from path or custom name; repository row typing moves to
shared ProjectRepositoryRow.
- getProjectPaths() returns only non-archived rows (isArchived = 0).
- shared/types.ts: ProjectRepositoryRow, CreateProjectPathResult/outcome,
WorkspacePathValidationResult.
- shared/utils.ts: WORKSPACES_ROOT, forbidden path lists, validateWorkspacePath,
asyncHandler for Express async routes.
Legacy cleanup
- server/projects.js: remove detectTaskMasterFolder, normalizeTaskMasterInfo,
and getProjectTaskMasterById (logic lives in the new service).
- server/routes/agent.js: register external API project paths with
projectsDb.createProjectPath instead of addProjectManually try/catch;
treat active_conflict as an existing registration and continue.
Tests
- Add Node test suites for project-management, project-clone, and
projects-has-taskmaster services; update projects.service test import
for renamed projects-with-sessions-fetch.service.ts.
Rename
- projects.service.ts → projects-with-sessions-fetch.service.ts;
re-export from modules/projects/index.ts.
Client (project creation wizard)
- Remove StepTypeSelection and workspaceType from form state and types;
wizard is two steps (configure path/GitHub auth, then review).
- createWorkspaceRequest → createProjectRequest; clone vs create-only
inferred from githubUrl (pathUtils / isCloneWorkflow).
- Adjust step indices, WizardProgress, StepConfiguration/Review,
WorkspacePathField, and src/utils/api.js as needed for the new API.
Docs
- Minor websocket README touch-up.
Net: ~1.6k insertions / ~0.9k deletions across 29 files; behavior is
centralized in typed services with explicit HTTP errors and test seams.
* refactor: remove loading sessions logic from sidebar
* refactor: move project rename to module
* refactor: move project deletion to module
* refactor: move project star state from localStorage to backend
* refactor: implement optimistic UI for project star state management
* feat: optimistic update for session watcher
* fix(projects-state): stop websocket message reprocessing loop
The websocket projects effect in useProjectsState could re-handle the same
latestMessage after local state writes triggered re-renders.
Under bursty websocket traffic, this created an update feedback cycle
that surfaced as 'Maximum update depth exceeded', often from Sidebar.
What changed:
- Added lastHandledMessageRef so each latestMessage object is handled once.
- Added an early return guard when the current message was already handled.
- Made projects updates idempotent by comparing previous and merged payloads
before calling setProjects.
Result:
- Breaks the effect -> state update -> effect re-entry cycle.
- Reduces redundant renders during rapid projects_updated traffic while
preserving normal project/session synchronization.
* refactor: optimize project auto-expand logic
* refactor: move projects provider specific logic into respective session providers
* refactor: move rename and delete sessions to modules
* refactor: move fetching messages to module
* fix: remove unused var
Co-authored-by: Copilot Autofix powered by AI <223894421+github-code-quality[bot]@users.noreply.github.com>
* Potential fix for pull request finding 'Useless assignment to local variable'
Co-authored-by: Copilot Autofix powered by AI <223894421+github-code-quality[bot]@users.noreply.github.com>
* refactor(projects/sidebar): remove temp snapshot side-effects and simplify session metadata UX
Why this change was needed:
- Project listing had an implicit side effect: every fetch wrote a debug snapshot under `.tmp/project-dumps`.
That added unnecessary disk I/O to a hot path, introduced hidden runtime behavior, and created maintenance
overhead for code that was not part of product functionality.
- Keeping snapshot-specific exports/tests around made the projects module API broader than needed and coupled
tests to temporary/debug behavior instead of user-visible behavior.
- Codex sessions could remain stuck with a placeholder name (`Untitled Codex Session`) even after a real title
became available from newer sync data, which degraded session discoverability in the UI.
- Sidebar session rows showed duplicated provider branding and long-form relative times, which added visual noise
and reduced scan speed when many sessions are listed.
What changed:
- Removed temporary projects snapshot dumping from `projects-with-sessions-fetch.service.ts`:
- deleted snapshot types/helpers and file-write flow
- removed the write call from `getProjectsWithSessions`
- Removed snapshot-related surface area from `projects/index.ts`.
- Removed the snapshot-focused test `projects.service.test.ts` that only validated removed debug behavior.
- Updated `codex-session-synchronizer.provider.ts` to upgrade session names when an existing session still has
the placeholder title but a real parsed name is now available.
- Updated `SidebarSessionItem.tsx`:
- removed duplicate provider logo rendering in each session row
- moved age indicator to the right side
- made age indicator fade on hover to prioritize action controls
- switched to compact relative time format (`<1m`, `Xm`, `Xhr`, `Xd`) for faster list scanning
Outcome:
- Lower overhead and fewer hidden side effects in project fetches.
- Cleaner module boundaries in projects.
- Better Codex session naming consistency after sync.
- Cleaner sidebar density and clearer hover/action behavior.
* refactor: implement pagination for project sessions loading
* refactor: move search to module
* fix: search performance
* refactor: add handling for internal Codex metadata in conversation search
* fix(migrations,projects,clone): normalize legacy schema before writes and harden conflict detection
Why
- Legacy installs can have a sessions table shape that predates provider/custom_name columns. Running migrateLegacySessionNames first caused its INSERT OR REPLACE INTO sessions (...) to target columns that may not exist and fail during startup migration.
- Some upgraded databases had projects.project_id as plain TEXT instead of a real PRIMARY KEY. That breaks assumptions used by id-based lookups and can allow invalid/duplicate identity semantics over time.
- projectsDb.createProjectPath inferred outcomes from
ow.isArchived, but the upsert path always returns the post-update row with isArchived=0, so archived-reactivation and fresh-create could be misclassified.
- git clone accepted user-controlled URLs directly in argv position, so inputs beginning with - could be interpreted as options instead of a repository argument.
What
- Added
ebuildProjectsTableWithPrimaryKeySchema in migrations: detect table shape via getTableInfo('projects'), verify project_id has pk=1, and rebuild when missing.
- Rebuild flow now creates a canonical projects__new table (project_id TEXT PRIMARY KEY), copies rows with transformation, backfills empty ids via SQLITE_UUID_SQL, deduplicates conflicting ids/paths, then swaps tables inside a transaction.
- Replaced the prior ddColumnToTableIfNotExists(...) + UPDATE project_id sequence with PK-aware detection/rebuild logic so legacy DBs converge to the required schema.
- Reordered migration sequence to run
ebuildSessionsTableWithProjectSchema before migrateLegacySessionNames, ensuring sessions is normalized before legacy session_names merge writes execute.
- Updated projectsDb.createProjectPath to generate an ttemptedId before insert, pass it into the prepared statement, and classify outcomes by comparing returned
ow.project_id to ttemptedId (created vs
eactivated_archived), with no-row remaining ctive_conflict.
- Hardened clone execution by inserting -- before clone URL in git argv and rejecting normalized GitHub URLs that start with - in startCloneProject.
Tests
- Added integration coverage for projectsDb.createProjectPath branches: fresh insert, archived reactivation, and active conflict.
- Added clone service test for option-prefixed githubUrl rejection (INVALID_GITHUB_URL).
* refactor(session-synchronizer): update last scanned timestamp based on synchronization results
* refactor: improve session limit and offset validation in provider routes
* refactor: normalize project paths across database and service modules
* refactor(database): make session id the primary key in sessions table
* fix(codex): preserve reasoning entries as thinking blocks
Codex history normalization was downgrading reasoning into plain assistant text
because of branch ordering, not because the raw data was missing.
Why this mattered:
- Codex reasoning JSONL entries are intentionally mapped to history items with
type thinking, but they also carry message.role assistant.
- normalizeHistoryEntry evaluated the assistant-role branch before the
thinking branch.
- As a result, reasoning content matched the assistant-text path first and was
emitted as kind text instead of kind thinking.
- This collapses semantic intent, so UI and downstream features that rely on
thinking blocks (separate rendering, filtering, and interpretation of model
thought process vs final answer) receive the wrong message kind.
What changed:
- Prioritized thinking detection (raw.type === thinking or raw.isReasoning)
before role-based assistant normalization.
- Kept a non-empty content guard for thinking payloads to avoid emitting empty
artifacts.
Impact:
- Reasoning entries from persisted Codex JSONL now remain thinking blocks
end-to-end.
- Regular assistant text normalization behavior remains unchanged.
* refactor: remove dead code
* refactor: directly use getProjectPathById from projectsDb
* refactor: add gemini jsonl session support
* feat: introduce opus 4.7
- Bump claude-agent-sdk from 0.2.59 to 0.2.116
- Forward process.env to SDK subprocess so ANTHROPIC_BASE_URL and other env vars work
- Add claude-opus-4-6 as a distinct mode
* feat: add "claude" as fallback in the cli path
* fix: base url config
* feat(i18n): add Turkish (tr) language support
Add comprehensive Turkish localization for the UI, following the
existing i18n pattern established by Japanese (#384), Russian (#514),
and German (#525) language support.
Changes:
- Add Turkish translation files for all 7 namespaces
(auth, chat, codeEditor, common, settings, sidebar, tasks)
- Register Turkish locale in config.js with all resources
- Add Turkish entry to languages.js (value: tr, nativeName: Türkçe)
- Update .gitignore to allow src/i18n/locales/tr/tasks.json
(matches existing en/ja/ru/de exceptions)
Translation details:
- 934 total strings translated (100% coverage, matches en.json key count)
- Translated by a native Turkish speaker with software engineering
background; terminology reviewed against conventional Turkish
tech community usage.
- Technical terms kept in English per Turkish dev community norms:
Claude, Cursor, Codex, Gemini, CLI, MCP, PRD, JSON, YAML, stdio,
http, commit, branch, token, prompt, minimap, sandbox, YOLO.
- Informal second-person singular (\"sen\") used throughout — fits the
developer-facing nature of the UI.
- All interpolation placeholders preserved exactly (e.g. {{count}},
{{projectName}}, {{email}}).
- i18next plural keys (_one/_other) kept intact.
Verification:
- Key structure parity with en.json confirmed (jq paths diff empty)
- All 38 unique interpolation variables preserved
- npm run build passes cleanly
* docs(readme): add Turkish README and language switcher links
Add README.tr.md — full Turkish translation of the main README,
following the structure of existing README.de.md / README.ja.md /
README.ko.md / README.ru.md / README.zh-CN.md.
Update the language switcher row in all 6 existing README variants
to include a Turkish link (matches the pattern used by #534 for the
German language link addition).
---------
Co-authored-by: Simos Mikelatos <simosmik@gmail.com>
Add complete Italian (it) translations for all 7 namespaces:
common, auth, settings, sidebar, chat, codeEditor, and tasks.
Register Italian in languages.js and i18n config.
Add .gitignore exception for it/tasks.json.
Co-authored-by: Simos Mikelatos <simosmik@gmail.com>