Skip to content

feat(chat): the chat is an editor tab, and only an editor tab - #81

Merged
ndemianc merged 1 commit into
developfrom
feat/chat-editor-only
Aug 16, 2026
Merged

feat(chat): the chat is an editor tab, and only an editor tab#81
ndemianc merged 1 commit into
developfrom
feat/chat-editor-only

Conversation

@ndemianc

Copy link
Copy Markdown
Contributor

Removes the contributed chat view in the right-hand bar entirely. Sessions keeps that container — an index of past conversations is a different thing from the conversation, and doesn't need to split a narrow column with it.

Why the view had to go, rather than be deprioritised

Two possible hosts for one conversation is what produced every bug reported against it:

  • closing the tab reopened the chat on the right
  • ⇧⌘I opened it on the right
  • the ResizeObserver console spam came from purpose=webviewViewthe sidebar copy

Each was fixable in isolation. The shape that kept generating them was not.

Gone with the view: the hand-over card (detachedHtml), the reattach message, the move command and its button, the close-versus-move distinction, and two of the three transcript-replay paths. levelcode.ai.focus (⇧⌘I) and every background reveal now open the tab.

Closing the tab is an ending, not a discard

It seals the live session into History and lets memory learn from it — the same m.seal('done') + enrichMemoryAsync(sealedId) New Chat has always done, now extracted into sealLiveSession() and shared.

Two copies would drift, and the half that drifted would be the close path, because that's the half nobody watches. It cannot throw: it runs from a dispose handler, where an exception has nowhere to go.

The buttons moved with the chat

New Chat, Add Files and Set API Key lived on the sidebar view's title bar. They're now on the chat tab's title bar, gated on activeWebviewPanelId == 'levelcode.ai.chat'.

Deleting the view without moving them would have deleted the only place they were reachable outside the palette — which is no place at all for a capability nobody knows exists. A guard also fails on any menu entry still scoped to the removed view, since a stale when is a button that appears nowhere.

Setting

chat.startLocation drops secondarySidebar; editor and none are the only honest values left. An existing secondarySidebar in settings.json falls back to the default through the validation that was already there.

Guards

Each bypass-verified by reverting the fix:

bypass caught
closing no longer seals — conversation silently dropped
memory never learns from the sealed session
newChat grows its own copy of the sealing logic again
⇧⌘I points back at the removed view
nothing constructs the chat provider (it still owns wire()/makeLive())
the chat returns as a contributed view ✓ (caught in sessionsUi, where that guard lives)

Two test corrections worth naming

The fire-and-forget scan was checking nothing. It looked for executeCommand('levelcodeAi.chat.focus') — a string that no longer appears anywhere — so it would have kept passing forever. Repointed at openChatInEditor(), and taught to skip the function declaration rather than flag it as an unhandled call.

sessionsUi asserted "both Chat and Sessions views present". It now asserts Sessions is there and Chat is not, which is the property that matters.

Supersedes #80

A close-versus-move distinction is meaningless when there's nothing to move to. #80 should be closed unmerged.

19 tests in chatSurface, 34 suites green.

Removes the contributed chat view in the right-hand bar entirely. Sessions keeps that
container — an index of past conversations is a different thing from the conversation, and
does not need to split a narrow column with it.

WHY THE VIEW HAD TO GO RATHER THAN BE DEPRIORITISED. Two possible hosts for one
conversation is what produced every bug reported against it: closing the tab reopened the
chat on the right, ⇧⌘I opened it on the right, and the ResizeObserver console spam came
from `purpose=webviewView` — the sidebar copy. Each was fixable in isolation; the shape
that kept generating them was not.

Gone with it: the hand-over card (detachedHtml), the `reattach` message, the move command
and its button, the close-versus-move distinction, and two of the three transcript-replay
paths. `levelcode.ai.focus` (⇧⌘I) and every background reveal now open the tab.

CLOSING THE TAB IS AN ENDING, NOT A DISCARD. It seals the live session into History and
lets memory learn from it — the same `m.seal('done')` + `enrichMemoryAsync(sealedId)` that
New Chat has always done, now extracted into sealLiveSession() and shared. Two copies would
drift, and the half that drifted would be the close path, because that is the half nobody
watches. It cannot throw: it runs from a dispose handler, where an exception has nowhere
to go.

The three actions that lived on the sidebar view's title bar — New Chat, Add Files, Set API
Key — move to the chat TAB's title bar, gated on
`activeWebviewPanelId == 'levelcode.ai.chat'`. Deleting the view without moving them would
have deleted the only place they were reachable outside the palette, which is no place at
all for a capability nobody knows exists.

`chat.startLocation` drops `secondarySidebar`; `editor` and `none` are the only honest
values left. An existing `secondarySidebar` in settings.json falls back to the default
through the validation that was already there.

Guards, each bypass-verified by reverting the fix:
  - closing no longer sealing, so the conversation is silently dropped
  - memory never learning from the sealed session
  - newChat growing its own copy of the sealing logic again
  - ⇧⌘I pointing back at the removed view
  - nothing constructing the chat provider (which still owns wire()/makeLive())
  - the chat returning as a contributed view (caught in sessionsUi, where that guard lives)

Two test corrections worth naming:
  - the fire-and-forget scan looked for executeCommand('levelcodeAi.chat.focus'), a string
    that no longer appears anywhere — it would have kept passing while checking nothing.
    Repointed at openChatInEditor(), which is what opens the chat now, and taught to skip
    the function declaration.
  - sessionsUi asserted "both Chat and Sessions views present". It now asserts Sessions is
    there and Chat is NOT, which is the property that matters.

Supersedes #80: a close-versus-move distinction is meaningless when there is nothing to
move to.

19 tests in chatSurface, 34 suites green.
Copilot AI lite review requested due to automatic review settings August 16, 2026 17:55

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

This PR makes the LevelCode AI chat editor-tab-only by removing the contributed sidebar Chat view, keeping the sidebar container exclusively for Sessions, and simplifying the chat lifecycle (open/focus/close) around a single WebviewPanel.

Changes:

  • Removed the contributed levelcodeAi.chat WebviewView surface and related “move/hand-off” machinery.
  • Ensured closing the chat tab seals the live session (shared sealing path with New Chat) and triggers memory enrichment.
  • Updated extension contributions and tests to reflect the editor-only chat surface and moved title-bar actions onto the chat tab.

Reviewed changes

Copilot reviewed 4 out of 4 changed files in this pull request and generated 1 comment.

File Description
extensions/levelcode-ai/extension.js Removes sidebar chat surface, re-routes focus/open behavior to editor tab, and adds shared session-sealing on close.
extensions/levelcode-ai/package.json Removes Chat view contribution and move command; relocates chat actions to the chat editor tab title bar.
extensions/levelcode-ai/test/chatSurface.test.js Updates wiring/guard assertions for editor-only chat and adds close-path sealing and “no sidebar resurrection” checks.
extensions/levelcode-ai/test/sessionsUi.test.js Updates view contribution assertions to ensure Sessions is present and Chat is not.
Suppressed comments (1)

extensions/levelcode-ai/extension.js:2370

  • The PR description says an existing chat.startLocation: "secondarySidebar" should fall back to the default via validation, but the current flow still allows that value through chatStartLocation() and then logs it. Even though it ultimately opens the editor tab, this means the setting does not actually “fall back” and diagnostics can misleadingly report where: secondarySidebar. Consider mapping the legacy value to editor before logging/branching.
	const where = chatStartLocation();
	dbg('chat.startLocation', { where });
	if (where === 'none') { return; }
	await openChatInEditor({ preserveFocus: true });
}

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment on lines 2328 to 2329
// Hand the sidebar slot over. Its listener survives an html swap, so the card's button still
// reaches the same handler — see ChatViewProvider.wire.
@ndemianc
ndemianc merged commit df1046d into develop Aug 16, 2026
2 checks passed
@ndemianc
ndemianc deleted the feat/chat-editor-only branch August 16, 2026 22:24
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants