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
- Click the header "+ New session" button.
- The conversation pane is empty; the empty-state cwd chooser appears.
- Click the chooser to open the "Select working directory" modal.
- 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:
- Cancel the picker.
- Send some throwaway message (or close the session).
- Create another new session — which still lands in the wrong cwd.
- …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)
-
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.
-
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.
-
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
Related
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:
…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
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.andselect.disabledis 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:
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
The check rejects switching whenever any message in the session has
role === "user". The hypothesis is that a seed / bootstrap / context-loader message withrole === "user"is already present before the user types anything — possibly injected by the agent's session manager duringcreateNewLiveSession()(orbuildSessionContext()'s replay path). That's invisible to the UI, but it polluteshasUserMessages().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:
Suggested fixes (any one is enough; first is preferred)
Distinguish bootstrap-user messages from real user input. Track a flag (e.g.
session.hasUserInput) set totrueonly when the composer's/api/messages(or equivalent) is hit. Use that flag instead of scanningsession.messagesforrole === "user". This is the most precise fix.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/cwdcould blow away the in-memory messages and re-create the session at the new cwd, instead of hard-failing.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
streamingandcompactingguards continue to fire when applicable.Related