Skip to content

Folder picker rejects 'Select folder' on a brand-new session ('Working directory can only be changed before the first message') #16

@goyamegh

Description

@goyamegh

Summary

The "Select working directory" picker, opened from the empty-cwd chooser on what looks (from the user's perspective) like a brand-new session, refuses to switch cwd with the error:

Working directory can only be changed before the first message.

…even though no message has been sent yet. The user has not typed anything; the conversation pane is empty; the picker was opened from the "empty session" call-to-action.

Repro

  1. Click the header "+ New session" button.
  2. The conversation pane is empty; the empty-state cwd chooser appears.
  3. Click the chooser to open the "Select working directory" modal.
  4. Navigate to any folder and click Select folder.

Expected: the picker accepts the cwd, the new session re-homes, the modal closes.

Observed: the picker shows the error Working directory can only be changed before the first message. and select.disabled is reset, so the user can't proceed. The only escape is Cancel.

(The screenshot I have shows this happening on /local/home/goyamegh/workplace/AESOncallClaudeCode-ws/src/AESOncallClaudeCode — a fresh session that had received zero composer input.)

Why this is contradictory

The empty-cwd chooser is only shown when the UI considers the session empty:

// src/sessions/sessionDrawer.ts (~line 149)
function updateEmptyCwdChooser() {
  elements.emptyCwdPathEl.textContent = state.currentCwd;
  elements.emptyCwdChooserEl.hidden =
    elements.messagesEl.children.length > 0 || state.isStreaming;
}

So the user is being offered an action that the server immediately refuses. The frontend's notion of "empty" (messagesEl.children.length === 0) and the server's notion (session.messages.some(m => m.role === "user") === false) have drifted out of sync.

Where the server-side check lives

// server.ts (~lines 229–233 and 1420–1426)
function hasUserMessages(value: PiWebSession) {
  return value.messages.some((message: any) => message?.role === "user");
}

async function switchEmptySessionCwd(cwd: string) {
  if (session.isStreaming) throw new Error("Wait for the current response to finish before changing the working directory.");
  if (session.isCompacting) throw new Error("Wait for compaction to finish before changing the working directory.");
  if (hasUserMessages(session)) throw new Error("Working directory can only be changed before the first message.");
  session = await createNewLiveSession(cwd);
  return currentStateWithThinkingLevels();
}

The check rejects switching whenever any message in the session has role === "user". The hypothesis is that a seed / bootstrap / context-loader message with role === "user" is already present before the user types anything — possibly injected by the agent's session manager during createNewLiveSession() (or buildSessionContext()'s replay path). That's invisible to the UI, but it pollutes hasUserMessages().

Why this matters

The empty-cwd chooser is the natural place to set the cwd of a new session. If it doesn't work, the user has to:

  1. Cancel the picker.
  2. Send some throwaway message (or close the session).
  3. Create another new session — which still lands in the wrong cwd.
  4. …and there's no further escape until they remember the per-folder "+" button in the drawer.

Suggested fixes (any one is enough; first is preferred)

  1. Distinguish bootstrap-user messages from real user input. Track a flag (e.g. session.hasUserInput) set to true only when the composer's /api/messages (or equivalent) is hit. Use that flag instead of scanning session.messages for role === "user". This is the most precise fix.

  2. Be permissive in the empty-chooser path. When the UI is showing the empty-cwd chooser, the user has clearly stated intent that the session is fresh. /api/session/cwd could blow away the in-memory messages and re-create the session at the new cwd, instead of hard-failing.

  3. Filter the check to non-seed user messages. If seed messages have a stable marker (synthetic flag, timestamp before session creation, custom block type), exclude them from hasUserMessages().

If the underlying agent really cannot move cwd after seed messages have been written, option (2) — recreate the session at the new cwd — is still safe because no real user input has been preserved.

Acceptance criteria

  • On a brand-new session (no composer input sent), opening the "Select working directory" modal and clicking Select folder succeeds.
  • The session continues at the chosen cwd, with empty conversation, no error.
  • After the first real user message has been sent, attempting to change cwd still surfaces the existing guard error (no regression of the original protection).
  • The streaming and compacting guards continue to fire when applicable.

Related

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions