Skip to content

Starting an AI chat in the docked terminal is now a first-class…#412

Merged
inkeep-oss-sync[bot] merged 1 commit into
mainfrom
copybara/sync
Jul 1, 2026
Merged

Starting an AI chat in the docked terminal is now a first-class…#412
inkeep-oss-sync[bot] merged 1 commit into
mainfrom
copybara/sync

Conversation

@inkeep-oss-sync

Copy link
Copy Markdown
Contributor

Starting an AI chat in the docked terminal is now a first-class action. A single Open chat / Close chat button in the editor header reveals the terminal (collapsing the right document panel so the chat gets the full column) and starts your default CLI when no session is open, or hides it when it's showing. The terminal tab strip's + is now New chat (launches your default CLI, next to the last tab) alongside a separate New terminal tab button for a plain shell.

Your default CLI is detected from what's installed — priority Claude → Codex → OpenCode → Cursor — and the one you pick anywhere sticks for next time; if none are installed it falls back to Claude with the existing install prompt. The "Ask AI" bubble and the file-tree / composer "Open in terminal" actions use the same default and also clear the document panel for the chat. Across the app, the Terminal option now leads the "Open with AI" menus ahead of Desktop.

…I detection (#2325)

* docs(open-knowledge): terminal-behaviors SPEC (audited, decisions locked)

* [US-001] Reorder TERMINAL_CLI_IDS and add promptless CLI launch build path

Reorder TERMINAL_CLI_IDS to claude,codex,opencode,cursor so the visible
launch-row order matches the default-CLI auto-pick priority (one constant
drives both). Consumer audit confirmed no site depends on the old positional
order (display map, VISIBLE_CLIS, and parseStickyCliId find-by-match are all
order-independent; no index access).

Widen buildCliLaunchArgString's prompt param to string|null|undefined and add
a promptless branch: an absent or empty prompt emits a bare <bin> (dropping the
positional and any prompt-carrying flag such as OpenCode's --prompt), keeping
only Claude's opted-in MCP pre-approval. This is the New chat launch shape. The
non-empty prompted path stays byte-identical.

* [US-002] Add resolveDefaultCli — shared default-CLI resolution

New pure function packages/app/src/lib/default-cli-resolver.ts that both the
New chat launch and the Ask X bubble default use, so every entry point agrees
on the same CLI. Resolution order:

  1. sticky parses to a CLI (parseStickyCliId) and that CLI is installed -> it;
  2. else the first installed CLI by TERMINAL_CLI_IDS priority;
  3. else claude (surfaces the existing Get Claude install nudge on PATH miss).

Priority order is TERMINAL_CLI_IDS itself, so auto-pick order stays in lockstep
with the visible launch-row order. An app-target sticky (a HandoffTarget id, not
a terminal-cli sentinel) parses to null and falls through — New chat only ever
launches a CLI. The installed map is Partial so a not-yet-resolved probe
degrades to the claude fallback. Covered by 10 unit tests.

* [US-003] Add batched CLI-install probe + renderer bridge exposure

New desktop probe answering which of the four CLIs are on PATH, so the New chat
default-CLI auto-pick can pick the first installed one.

claude-readiness.ts: pure resolveCliInstalledMap(deps) -> Record<TerminalCli,
boolean> (installed iff the login-shell command -v probe exits 0). Each entry
routes through the existing resolveCliOnPath, so a flaky or rejected probe
degrades that one entry to not-installed without crashing the batch; unknown
collapses to false since defaulting must resolve to a concrete CLI. Covered by 3
unit tests over the exit-code to installed-map parsing.

Renderer exposure co-located with cliPreflight: new cliInstalledMap() bridge
method added to all three OkDesktopBridge copies (canonical bridge-contract plus
the core and app mirrors, kept in lockstep for the m1-smoke drift catcher), the
ok:terminal:cli-installed-map IPC channel, the preload invoke wiring, and a main
handler backed by a 60s promise-cache so repeated New chat clicks share one probe
batch instead of re-spawning four login shells each time.

The stress fixture bridge fake gains the method to satisfy the contract.

* [US-004] Wire EditorPane for New chat: terminalAvailable, promptless launch, installed-map

Derive a single terminalAvailable = desktopBridge != null && desktopBridge.terminal
!= null and gate both the terminal host mount and the header terminal buttons on
it, so a session-only bridge (no terminal surface) never renders a control that
would throw.

Widen TerminalLaunchIntent.prompt to string | null — null is a New chat launch.
It flows through TerminalSessionsHost, TerminalGate, and TerminalPanel, which
already passes it to the now-null-safe buildCliLaunchArgString.

Fetch the installed-CLI map once on mount (capability-guarded like the existing
getDockState restore, so a partial or older bridge skips the probe rather than
throwing a synchronous not-a-function the .catch cannot intercept). launchNewChat
resolves the CLI via resolveDefaultCli(sticky, installed) unless one is named,
reveals the dock, and threads a promptless one-shot intent through the same
nonce-dedup + hide-clear path as Open in terminal.

Thread terminalAvailable / terminalVisible / onToggleTerminal / onNewChat to
EditorHeader and the installed-CLI map to TerminalSessionsHost (the header buttons
render in US-005, the tab-strip New chat in US-006). The EditorPane dom-test stub
now models cliInstalledMap.

* [US-005] Add New chat + show/hide terminal buttons to the editor header

When the terminal is available (desktop bridge + terminal surface), the header
now renders, after the presence badges and before the vertical separator, a New
chat button and a show/hide-terminal toggle. Both are ghost icon buttons matching
the existing Search control: an accessible aria-label, an aria-hidden icon, a
tooltip, and the electron no-drag opt-out.

New chat (MessageSquarePlus) launches the resolved default CLI promptless via the
onNewChat handler. The toggle (Terminal) flips the dock visibility; its aria-label
and tooltip name the action they perform, flipping between Show terminal and Hide
terminal with dock state (the WAI-ARIA changing-label toggle pattern — no
aria-pressed, which would double-announce state alongside a Show/Hide label). All
copy routes through Lingui. Neither button renders on web or a bridge without a
terminal surface.

Covered by four DOM tests: button order, label flip, handler dispatch, and
absence when the terminal is unavailable.

* [US-006] Split the tab-strip + into New chat and New terminal tab

The tab strip's single + button becomes two controls. New chat (keeps the plus
icon) hugs the last tab and launches the default CLI promptless; a new New
terminal tab button (SquareTerminal) sits in the trailing control group and opens
a bare shell, the previous + behavior.

Layout: TabsList drops flex-1 (keeps min-w-0 + overflow-x-auto so tabs still
scroll internally) so New chat can sit immediately right of the last tab, outside
the scroll container's fade mask. A flex-1 spacer then pushes the trailing group
(New terminal tab, dock-toggle, collapse) to the far right.

TerminalSessionsHost resolves the strip New chat's CLI from the sticky pick plus
the installed-CLI map at click time (resolveDefaultCli) and opens a promptless
session; New terminal tab keeps openSession(null). The launch nonce comes from a
local monotonic ref — it is required by the intent type but only load-bearing for
the EditorPane prop-channel dedup, so a direct open just needs a distinct value.

Updates TerminalTabStrip's tests for the renamed/added controls and fixes the
TerminalDock multi-session tests, which created bare-shell sessions through the
old + and now go through New terminal tab.

* [US-007] Default the Ask X bubble to the first-installed CLI on desktop

On desktop, when the user has made no pick and has no sticky preference, the Ask
X bubble now leads with the first-installed CLI (resolveDefaultCli; Claude if
none) instead of the first installed app target — matching what New chat would
launch. The composer fetches its own installed-CLI map from the terminal bridge
(capability-guarded, main-cached) to drive this.

The default is derived, never saved, so a freshly-installed CLI can take over
mid-session. An explicit CLI pick still launches that CLI regardless of install
state (the existing no-probe contract for the picker), a sticky app-target pick
is still honored, and the web host keeps the app-target default.

Two DOM tests cover the desktop no-pick default: Codex when Claude is absent, and
the Claude CLI fallback when nothing is installed.

* [US-008] Lead the Open with AI dropdowns with the Terminal section

Flip the two sections in all five Open with AI surfaces so Terminal renders
before Desktop, leading with the in-app terminal as the first-class path:
OpenInAgentMenu, OpenInAgentContextSubmenu, OpenInAgentEmptySpaceSubmenu,
AgentSplitButton, and the empty-state CreatePromptComposer.

Each swap moves the Terminal block above the Desktop block and relocates the
inter-section separator to sit after Terminal (gated on the Terminal section
being present) instead of before it. Option lists, labels, test ids, and
accessible names are otherwise unchanged; the Command Palette (no Terminal
section) is untouched.

Adds Terminal-precedes-Desktop DOM-order assertions to the four existing section
grouping tests and a new order assertion on the split-button picker in
BottomComposer's tests. No new user-facing strings.

* [US-009] Regenerate Lingui catalogs for the new terminal strings

Runs bun run i18n to extract and compile the en + pseudo catalogs for the
strings added by the terminal-behaviors work: New chat, Show terminal, Hide
terminal, and New terminal tab. The obsolete New terminal tab-strip tooltip
(renamed to New chat / New terminal tab) is dropped by the extractor.

Also strips spec-marker citations from a handful of test comments (that linkage
belongs in the commit, not the source).

* feat(open-knowledge): consolidate header chat control to Open/Close toggle + collapse doc-panel on chat open

- Replace the two header buttons (New chat + show/hide terminal) with one
  stateful toggle: Open chat -> Close chat, reflecting dock visibility.
- Opening the chat (header toggle, file-tree right-click, bottom composer)
  collapses the right doc-panel so the chat gets the full column; deferred a
  frame so it lands after the terminal column mounts, and it never touches the
  panel on close. Panel stays user-reopenable.
- Report session presence from the host so the toggle bootstraps a default-CLI
  chat only when none exists.

* docs(open-knowledge): record D9/D10 header-chat amendments (supersede D3/D5)

* fix(open-knowledge): review fixes — honor sticky CLI pre-probe, transient/dock-guarded doc-panel collapse

- resolveDefaultCli: honor a sticky CLI unless KNOWN-absent (false), so a New
  chat click during the cold-start probe window keeps the remembered pick
  instead of dropping to claude — matching the Ask-X bubble. Priority auto-pick
  now requires a positive install (true).
- Doc-panel collapse on chat open: stop writing the persistent 'collapsed' pin
  (it left the panel collapsed across docs + restarts after one chat/ask-AI);
  now transient. Guard on right-dock (a bottom-docked chat has no right column
  to clear). Cancel the deferred rAF on cleanup/re-request.

* chore(open-knowledge): changeset for terminal chat controls

* fix(open-knowledge): warn (not error) for recoverable CLI-install probe failure

The cliInstalledMap probe failure degrades to 'none installed' → claude default,
so it's recoverable. Log at console.warn with the [terminal] namespace to match
the terminal surface's probe-failure convention (TerminalPanel, ClaudeReadinessBanner)
rather than error severity + an [ask-ai] namespace that breaks [terminal] triage.

* refactor(open-knowledge): extract shared useInstalledClis hook; tidy bridge JSDoc

Review feedback: the cliInstalledMap probe effect (state + capability guard +
cancel-flag cleanup + error log) was duplicated byte-for-byte in EditorPane and
BottomComposer, both feeding the same resolveDefaultCli. Extract a shared
useInstalledClis() hook (matching the useGitSyncStatus bridge-hook precedent) so
both are a single call and a third consumer reuses it. Strip the lone JSDoc on
cliInstalledMap in the core desktop-bridge mirror to match its bare siblings.

* test(open-knowledge): pin useInstalledClis contract; evict CLI-map cache on probe rejection

Review follow-ups:
- Add use-installed-clis.dom.test.tsx covering the hook's three behaviors: the
  capability guard (web-host / partial-bridge → empty map, no throw), the
  resolved-map success path, and the .catch degradation (rejected probe → empty
  map + warn).
- resolveTerminalCliInstalledMap now evicts the TTL cache on a rejected probe
  (defensive — the batch never rejects today) so a transient failure becomes an
  immediate retry rather than a 60s-cached rejection.

---------

GitOrigin-RevId: c4b03c63913e4ec4c42dc36d3dd2d2a01c23ac99

@inkeep-internal-ci inkeep-internal-ci Bot 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.

Automated approval from agents-private public-mirror-sync (run: https://github.com/inkeep/agents-private/actions/runs/28500970708). Source of truth is the monorepo; direct edits on inkeep/open-knowledge are overwritten on next sync.

@inkeep-oss-sync
inkeep-oss-sync Bot merged commit 69c94ad into main Jul 1, 2026
@inkeep-oss-sync
inkeep-oss-sync Bot deleted the copybara/sync branch July 1, 2026 07:28
@CLAassistant

Copy link
Copy Markdown

CLA assistant check
Thank you for your submission! We really appreciate it. Like many open source projects, we ask that you sign our Contributor License Agreement before we can accept your contribution.
You have signed the CLA already but the status is still pending? Let us recheck it.

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