Skip to content

feat(tui): show active skills in session sidebar - #61491

Open
MrDrMcCoy wants to merge 6 commits into
anomalyco:devfrom
MrDrMcCoy:active-skills-sidebar
Open

MrDrMcCoy wants to merge 6 commits into
anomalyco:devfrom
MrDrMcCoy:active-skills-sidebar

Conversation

@MrDrMcCoy

@MrDrMcCoy MrDrMcCoy commented Nov 3, 2026 •

Copy link
Copy Markdown

Issue for this PR

Closes #48355

Type of change

  • Bug fix
  • New feature
  • Refactor / code improvement
  • Documentation

What does this PR do?

This PR adds an Active Skills panel to the TUI session sidebar. It lists only the skills the current conversation already contains, whether loaded through the skill tool or attached directly to a prompt, and labels each source as built-in, project, global, or external.

Loaded skills are derived once in @opencode/schema/session-message and shared by the sidebar and the session core. The skill tool result carries a short persisted note so the model does not load the same skill twice; because the note is part of the conversation, it does not move the prompt-cache breakpoint. Compaction filters the loaded set with the agent and session permission rules and asks the summary model to carry the names forward.

This ports the closed V1 pull request #48461 to the V2 session core and TUI plugin architecture.

How did you verify your code works?

  • bun test for the session skills, generate, compaction, tool-skill, runner, and sidebar suites
  • bun run typecheck in packages/core and packages/tui
  • Live TUI check with bun run dev --standalone .

Visual

Active Skills sidebar panel

SVG source
<svg xmlns="http://www.w3.org/2000/svg" width="240" height="72" viewBox="0 0 240 72" role="img" aria-label="Active Skills panel in the session sidebar, with a newly loaded skill appearing">
  <style>
    :root { color: #24292f; color-scheme: light dark; }
    .bg { fill: #ffffff; }
    .panel { fill: currentColor; opacity: .05; }
    .frame { fill: none; stroke: currentColor; stroke-opacity: .2; }
    .rule { stroke: currentColor; stroke-opacity: .12; }
    .title { fill: currentColor; font: 700 9px ui-monospace, SFMono-Regular, Menlo, monospace; }
    .row { fill: currentColor; font: 9px ui-monospace, SFMono-Regular, Menlo, monospace; }
    .muted { opacity: .5; }
    .bullet { fill: currentColor; }
    .enter { animation: enter 4.8s ease-out infinite; transform-box: fill-box; transform-origin: left center; }
    .pulse { animation: pulse 2.4s ease-in-out infinite; }
    @keyframes enter {
      0%, 8% { opacity: 0; transform: translateX(-6px); }
      24%, 92% { opacity: 1; transform: translateX(0); }
      100% { opacity: 0; transform: translateX(-2px); }
    }
    @keyframes pulse {
      0%, 100% { opacity: .55; }
      50% { opacity: 1; }
    }
    @media (prefers-color-scheme: dark) {
      :root { color: #f0f6fc; }
      .bg { fill: #0d1117; }
    }
    @media (prefers-reduced-motion: reduce) {
      .enter { animation: none; opacity: 1; transform: none; }
      .pulse { animation: none; opacity: .9; }
    }
  </style>
  <rect class="bg" width="240" height="72" rx="4"/>
  <rect class="panel" x="4" y="4" width="232" height="64" rx="4"/>
  <rect class="frame" x="4.5" y="4.5" width="231" height="63" rx="3.5"/>
  <text class="row" x="14" y="19">▼</text>
  <text class="title" x="24" y="19">Active Skills</text>
  <line class="rule" x1="10" y1="25" x2="230" y2="25"/>
  <g>
    <circle class="bullet pulse" cx="15" cy="37" r="2"/>
    <text class="row" x="22" y="40">effect</text>
    <text class="row muted" x="226" y="40" text-anchor="end">project</text>
  </g>
  <g>
    <circle class="bullet" cx="15" cy="50" r="2"/>
    <text class="row" x="22" y="53">report</text>
    <text class="row muted" x="226" y="53" text-anchor="end">built-in</text>
  </g>
  <g class="enter">
    <circle class="bullet" cx="15" cy="63" r="2"/>
    <text class="row" x="22" y="66">tdd</text>
    <text class="row muted" x="226" y="66" text-anchor="end">project</text>
  </g>
</svg>

Screenshots / recordings

Here is what it looks like in the TUI sidebar (highlighter added over section):

Screenshot_20261103_140234

Checklist

  • I have tested my changes locally
  • I have not included unrelated changes in this PR

Added by the opencode bot (AI-generated).

Related: #48252, #50606, #54346

Public API: adds LoadedSkill and loadedSkills() to @opencode/schema/session-message (backwards compatible)

Add an Active Skills panel to the TUI session sidebar. It lists only the
skills the current conversation already contains, whether loaded through
the skill tool or attached directly to a prompt, and labels each source
as built-in, project, global, or external.

Keep the model from loading the same skill twice. SessionSkills derives
the loaded set from message history, filters it with the agent and
session permission rules, and appends a compact <loaded_skills> reminder
to the system prompt. Compaction asks the summary model to carry the same
skill names forward so the information survives a long session.

This ports the closed V1 pull request anomalyco#48461 to the V2 session core and
TUI plugin architecture.
Expand the sidebar tests to cover built-in, global, and external source
labels, collapse and expand for more than two loaded skills, direct skill
activation, stale and malformed tool calls, reactive updates, permission
changes, and the built-in plugin slot registration.

Cover the session wiring too: generation and runner requests now assert
the <loaded_skills> system reminder, and manual compaction asserts the
summary prompt carries the skill names. SessionSkills also covers first
name wins and a specific allow after a wildcard deny.

@opencodejr opencodejr 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.

The sidebar panel and the <loaded_skills> reminder both work as described. I loaded a project skill through the skill tool with the simulated model. On the base there is no panel and no reminder. With this PR the sidebar lists demo-skill as project, and the next model request contains the reminder. The five touched core test files pass (303 tests), and the new sidebar test passes (10 tests). I couldn't run the type checks here because bun check isn't available in this Bun version.

Public API: unchanged. There is a new built-in TUI plugin, opencode.sidebar.skills, and the model now sees different prompt text.

Concerns (details inline):

  • Prompt caching (main concern): the reminder is a new system part that changes every time another skill loads. That invalidates the provider's cached prefix for the whole conversation after it, on every new skill load. The system prompt already tells the model that a skill already present in the conversation doesn't need to be invoked again, and the loaded content stays in history. Please justify the extra system part, or move the reminder somewhere that doesn't change the cached prefix.
  • Duplicated logic: the sidebar copies the core logic that works out which skills are loaded, so the two will drift apart.
  • Reminder wording: it says skills were loaded "via the skill tool", which is wrong for skills attached directly to a prompt.
  • Tests: sidebar-skills.test.tsx is 367 lines for a 95-line component, with a large hand-built context. It also passes session and agent permission options the component never reads. Please cut it down to the core cases: lists loaded skills, ignores calls that didn't complete, and labels sources.

The PR description has no before/after screenshots or animated SVG under ### Visual. Screenshots from this run are attached below.

Related: #48252, #50606, #54346

Before

After

Comment thread packages/core/src/session/skills.ts Resolved
The reminder was appended as a system part, so every new skill load
changed the system prompt and invalidated the provider's cached prefix
for the whole conversation behind it.

Append it as a transient user message at the tail instead, the same
place the goal plugin puts its reminder. The reminder text no longer
names the skill tool, because a skill can also arrive attached to a
prompt.

Move the derivation of loaded skills into the session message schema so
the sidebar and the model prompt share one definition. Core still
applies the agent and session permission rules on top.
The sidebar walked message history itself to decide which skills the
conversation already contains, so it could drift from the core
derivation when a skill message, prompt attachment or skill tool call
changed shape. Read the shared schema derivation instead.

Cut the sidebar test from 367 lines to the cases that matter: the
loaded list with each source label, incomplete skill calls, reactive
updates, and collapse. The hand-built context no longer supplies
session and agent permissions the component never reads.

@opencodejr opencodejr 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.

Thanks for the update. The sidebar still works: after a skill-tool load in the TUI it lists demo-skill as project. The core tests (303) and the slimmed-down sidebar test (4) pass. Sharing SessionMessage.loadedSkills in @opencode/schema fixes the duplicated logic, and the reminder no longer names the skill tool as the source.

Public API: @opencode/schema/session-message gains a LoadedSkill type and a loadedSkills() function (additions only, backwards compatible). The TUI has a new built-in plugin, opencode.sidebar.skills.

Moving the reminder out of the system prompt doesn't fix the caching problem, and for Anthropic it makes it worse (details inline). The reminder is now an unsaved user message at the end of every request once a skill is loaded, and the cache breakpoint on the last message lands on it, so the conversation cache isn't reused on any step.

Before

After

Comment thread packages/core/src/session/skills.ts Resolved
The transient <loaded_skills> user message at the request tail moved the
conversation cache breakpoint onto a message that was not in the next
request at that position. On Anthropic that discarded the conversation
cache on every step after a skill load.

Persist the note in the skill tool result instead. It stays at a fixed
point in history, so later tool results and the tail breakpoint reuse the
same prefix. Drop the per-request reminder from generation and the runner,
along with the now-unused render and remind helpers.

The existing skill guidance already tells the model that a loaded skill
does not need a second invocation, and compaction still carries the skill
names forward.

@opencodejr opencodejr 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.

The caching concern is resolved. The loaded-skill note now sits only at the end of the saved skill tool result. In a TUI run with the simulated model, the request after a skill load has no reminder as a system part or as a message at the end, and the sidebar still lists demo-skill as project. The core tests (301, including tool-skill.test.ts) and the sidebar test (4) pass. I couldn't run the type checks here because bun check isn't available.

Public API: @opencode/schema/session-message gains a LoadedSkill type and a loadedSkills() function, and the TUI has a new built-in plugin, opencode.sidebar.skills. These are additions only, so they're backwards compatible.

One small cleanup: the diff still has formatting-only leftovers, a re-wrap of the experimental.idle hook line in runner/llm.ts and a trailing blank line in session-model-request.test.ts. Both can be reverted so the change only touches what it needs.

Related: #48252, #50606, #54346

Before

After

@opencodejr opencodejr 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.

The last point is addressed: the formatting-only edits in runner/llm.ts and session-model-request.test.ts are reverted, and the PR no longer touches those files. This commit doesn't change behaviour, so I didn't rerun anything; my verification of the previous commit still applies. I have no further concerns.

Public API: unchanged since my last review. @opencode/schema/session-message gains LoadedSkill and loadedSkills(), and the TUI has a new built-in plugin, opencode.sidebar.skills. These are additions only, so they're backwards compatible.

@opencodejr

opencodejr Bot commented Nov 4, 2026

Copy link
Copy Markdown
Contributor

Thanks, @MrDrMcCoy. I checked all three in my last two reviews:

  • Caching: at 307d7fa the request after a skill load had no reminder, either as a system part or appended to the end. The only added text was the <skill_loaded> note in the saved tool result.
  • Wording: the note no longer names a source, and prompt-attached skills already have their <skill_content> in history.
  • Shared logic: the sidebar and SessionSkills.loaded both use SessionMessage.loadedSkills, so there's one copy.

I have nothing further from my side.

@MrDrMcCoy

Copy link
Copy Markdown
Author

The last message from the review bot was unclear. Is anything more needed from my end, or are we waiting for human review?

@opencodejr

opencodejr Bot commented Nov 6, 2026

Copy link
Copy Markdown
Contributor

Sorry that wasn't clear, @MrDrMcCoy. Nothing more is needed from you for my review. All of my earlier points are addressed and I have no open concerns at cdfe935.

The next step is a review by a maintainer, who decides on merging. One thing they may want to confirm themselves: I couldn't run the typecheck scripts in my environment (bun check wasn't available there), so the type checks you ran locally are the only ones so far.

This branch has not been deployed

No deployments
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.

[FEATURE]: Show active session skills in the UI (sidebar, like MCP/TODO/LSP) and keep the model aware of them during the session

1 participant