| FazBrowse GitHub Viewer | Trending | | Home |
| Tools: [Download Repo ZIP] [Original HTTPS Page] |
| Name | Name | Last commit date | ||
|---|---|---|---|---|
A runnable IntelliJ plugin that reproduces the Terminal tool window's shape - many renamable session tabs on the header line, controls after the tab strip, and controls right-aligned on that same line - using only public IntelliJ Platform API.
Tab + facsimile. It copies the Terminal's tab UX and nothing else.
Swttch/swttch#301 asked for a header that "functioned basically exactly like the Terminal": one bar instead of two, tabs that can be renamed, a few icon buttons, and a right-floating control that could still show a line of status text.
It was closed with:
That's a great idea, but unfortunately, this is handled natively by JetBrains IDEs, so it cannot be customized via plugins.
That is not the case, and this repo is the demonstration rather than the argument. Clone it, run ./gradlew runIde, open the Tabsimile tool window at the bottom.
A tool window named Tabsimile, docked bottom, whose header is one line:
Tabsimile Session 1 | Chat 2 x | Session 3 x [+] [v] Hello! [=] - gear/hide - \__ id __/ \____________ tabs ______________/ \_ tab _/ \___ title ___/ \_ platform _/ label actions actions
Twelve small classes, grouped by which half of the header they serve:
dev.simplified.tabsimile TabsimileToolWindowFactory, TabsimileIcons
dev.simplified.tabsimile.session SessionManager, SessionPanel, and the three
actions that create or rename tabs
dev.simplified.tabsimile.header HeaderMenuAction, GreetingLabelAction,
GreetingState, and the two toggles the menu
holds - ToggleGreetingAction and
ToggleIdLabelAction
Icons are SVG files under src/main/resources/icons, drawn for this repo rather than borrowed from AllIcons, so nothing on screen depends on a platform icon constant that may be renamed. A test asserts every icon path named in plugin.xml or loaded by TabsimileIcons resolves to a file that actually ships.
| Asked for | Possible | The API |
|---|---|---|
| Many tabs in one tool window | yes | ToolWindow#getContentManager + ContentFactory#createContent + ContentManager#addContent |
| Tabs renamable by the user | yes | subclass ToolWindowTabRenameActionBase, add to ToolWindowContextMenu |
| Per-tab close button | yes | canCloseContents="true" + Content#setCloseable |
| Show or hide the tool window name, live | yes | ToolWindowContentUi.HIDE_ID_LABEL + ToolWindowEx#updateContentUi |
| Buttons after the tabs | yes | ToolWindowEx#setTabActions |
| A dropdown floated right | yes | ToolWindow#setTitleActions |
| Text label floated right, toggleable | yes | setTitleActions + CustomComponentAction |
| Right controls stay visible with tabs | yes | ToolWindowContentUi.DONT_HIDE_TOOLBAR_IN_HEADER |
| An icon on the header line | per tab | Content#setIcon + ToolWindow.SHOW_CONTENT_ICON |
| Drag a tab to reorder | yes | ToolWindowContentUi.ALLOW_DND_FOR_TABS |
| A plugin icon left of the tabs | no | HEADER_ICON is @ApiStatus.Internal and unread on this path |
| Replacing the gear / hide cluster | no | the platform always appends it last |
This is the load-bearing fact, and it is checkable in any IntelliJ install at plugins/terminal/lib/terminal.jar!/META-INF/plugin.xml:
<toolWindow id="Terminal" anchor="bottom"
icon="org.jetbrains.plugins.terminal.TerminalIcons.OpenTerminal_13x13"
factoryClass="org.jetbrains.plugins.terminal.TerminalToolWindowFactory"
secondary="false" canCloseContents="true" />org.jetbrains.plugins.terminal is a bundled plugin using the ordinary com.intellij.toolWindow extension point. There is no private registration path. Disassembling TerminalToolWindowManager shows its session tabs come from ContentFactory.getInstance().createContent(...), ContentManager.addContent(...) and Content.setDisplayName(...) - the same public calls SessionManager makes.
The same is already true inside Swttch itself. Its own plugin.xml registers on the same extension point with the same attribute, and its own comment describes the behaviour:
<!-- Sidebar button: opens an editor tab (EDITOR_TAB mode) or hosts chat
content tabs directly (TOOL_WINDOW mode). canCloseContents enables the
per-tab close button used in TOOL_WINDOW mode. -->
<toolWindow id="Claude Code" anchor="right" icon="/icons/claudeCode.svg"
canCloseContents="true"
factoryClass="...ClaudeCodeToolWindowFactory"/>ToolWindowHost.kt already calls createContent / addContent and already sets the HideIdLabel client property. What is missing is only the header-action half - setTitleActions and setTabActions appear nowhere in the repository.
The part most worth knowing. The in-place rename popup is platform code in a public package, so the whole feature is a subclass with a constructor:
public final class RenameSessionAction extends ToolWindowTabRenameActionBase {
public RenameSessionAction() {
super(TabsimileToolWindowFactory.TOOL_WINDOW_ID, "Session name:");
}
}registered with:
<action id="Tabsimile.RenameSession" class="...RenameSessionAction" text="Rename Session...">
<add-to-group group-id="ToolWindowContextMenu" anchor="last"/>
</action>ToolWindowTabRenameActionBase carries no @ApiStatus annotation. Its default hooks already read Content#getDisplayName and write Content#setDisplayName, and it anchors a balloon with a text field over the tab label under the cursor. The tool window id passed to the constructor is what confines the menu entry to your own tabs. The Terminal's RenameTerminalSessionAction is the same subclass.
The header is not one undifferentiated bar. Two different methods target two different slots, and only one of them is on ToolWindowEx:
// after the tab strip - the Terminal's "+" and chevron live here
((ToolWindowEx) toolWindow).setTabActions(tabActionGroup);
// right-aligned on the same line, before the platform's gear and hide buttons
toolWindow.setTitleActions(List.of(new GreetingLabelAction(), new HeaderMenuAction()));setTitleActions and setAdditionalGearActions are declared on the plain ToolWindow interface. The only @ApiStatus.Internal methods on that entire interface are getStripeTitleProvider and getStripeShortTitleProvider.
Because a header slot is an ordinary action toolbar, it renders whatever component a CustomComponentAction hands it - so the right side is not limited to icon buttons. That is what makes the demo's round trip work:
The greeting state is held in memory and deliberately not persisted - there is no PersistentStateComponent, so it resets with the IDE.
One caveat that matters: in the New UI the right-hand toolbar fades out unless the tool window is hovered or focused. DONT_HIDE_TOOLBAR_IN_HEADER is the documented opt-out and this plugin sets it, which is what keeps the menu button pinned in every window rather than only the focused one.
The menu's second entry, ToggleIdLabelAction, shows and hides the tool window's own name at the far left. Showing it is the absence of a call: the platform paints that id label unless ToolWindowContentUi.HIDE_ID_LABEL is set on the tool window's component, so nothing here adds a component. Setting it gives the tabs the whole line, which is the shape the Terminal ships.
That it flips live is the part worth knowing. ContentLayout#shouldShowId re-reads the client property on every layout pass rather than caching it at construction, so the property is itself the state - no service, no field - and the only remaining step is telling the header to lay out again:
toolWindow.getComponent().putClientProperty(ToolWindowContentUi.HIDE_ID_LABEL, state ? null : "true");
if (toolWindow instanceof ToolWindowEx ex) ex.updateContentUi();updateContentUi is a public default method on ToolWindowEx, present on 233 and 253 alike. The property is cleared rather than written "false" because the platform tests "true".equals(...), so anything else already reads as showing and clearing leaves nothing stale behind.
The two toggles therefore sit in one menu reaching deliberately different scopes. The greeting is application-wide and backed by a service because nothing in the platform holds that flag; the name label is per tool window and backed by nothing, because the platform already holds it.
Being straight about the limits is the point of a proof:
Drag-to-reorder is opted into with ToolWindowContentUi.ALLOW_DND_FOR_TABS, which is marked deprecated from 253 in favour of ALLOW_TABS_REORDERING. That reads worse than it is. In 253's static initializer the two names are one object:
ALLOW_DND_FOR_TABS = Key.create("AllowDragAndDropForTabs");
ALLOW_TABS_REORDERING = ALLOW_DND_FOR_TABS;The deprecation is a rename, not a change of behaviour, and ToolWindowContentUi.setAllowTabsReordering(toolWindow, true) compiles down to exactly the same putClientProperty. The renamed field is @ApiStatus.Internal and both it and the helper exist only from 253, so the deprecated spelling is the only one of the three that reaches this plugin's 233 floor. PlatformApiSurfaceTest asserts the two fields are still the same object, so the day that stops being true the build says so.
The platform ANDs that property with the registry flag ide.allow.split.and.reorder.in.tool.window, which ships enabled in 233, 243 and 253 alike - verified in each build's misc/registry.properties. That half is a global user preference, so this plugin reads it and never writes it. The alternative, if you would rather not depend on a user-flippable flag, is what the Terminal also ships: explicit Move Tab Left / Move Tab Right actions over ContentManager.
./gradlew runIde # sandbox IDE with the plugin loaded
./gradlew test # the API-surface pins, no IDE fixture needed
./gradlew verifyPlugin # Plugin Verifier against IC 2023.3 / 2024.3 / 2025.3
./gradlew buildPlugin # -> build/distributions/Tabsimile-1.0.0.zip
./gradlew publishAndPackage # Maven Local + a Central-shaped staging bundlesrc/test is deliberately reflective rather than functional. The claim this repo makes is about an API surface, so the test pins that surface: every method, field and constructor named above fails the build if it is renamed, moved or narrowed.
JetBrains' own Plugin Verifier is the impartial referee here, and it reports:
Plugin dev.simplified.tabsimile:1.0.0 against IC-233.11799.241: Compatible
Plugin dev.simplified.tabsimile:1.0.0 against IC-243.21565.193: Compatible
Plugin dev.simplified.tabsimile:1.0.0 against IU-253.28294.334: Compatible. 1 usage of deprecated API
Deprecated API usages (1):
Deprecated field ToolWindowContentUi.ALLOW_DND_FOR_TABS is accessed in
TabsimileToolWindowFactory.configureHeaderLayout(ToolWindow)
Compatible on all three, and the single flagged item is the deliberate one explained above. Worth noting for what is absent: the verifier reports internal-API usage as its own category, and it raises none - so HIDE_ID_LABEL and DONT_HIDE_TOOLBAR_IN_HEADER are not internal API by JetBrains' own tooling.
Every claim here was checked against IntelliJ jars on disk - IC-233.11799.241, IC-243.21565.193 and IU-253.28294.334 - by disassembling the platform and the bundled Terminal plugin, not by reading documentation. The working notes, including the bytecode that settles each point, are in notes/toolwindow-tabs-evidence.md.
| Back | FazBrowse Home | New Git URL |