Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 38 additions & 0 deletions .claude/skills/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
# Claude Code Skills for temporal-plugin

Invoke any skill in Claude Code with `/<skill-name>`.

## Plugin-development skills (this repo's stack)

| Skill | Purpose |
|---|---|
| `/intellij-plugin-dev` | General IntelliJ Platform plugin-dev rules (sourced from the official JetBrains SDK docs). Entry point with topic files: `structure.md`, `extensions.md`, `services.md`, `threading.md`, `psi.md`, `indexes.md`, `inspections.md`, `actions-ui.md`, `gradle.md`, `kotlin.md`. |
| `/kotlin-dev` | Project-specific Kotlin conventions (common vs. languages packages, EP pattern, PHP mixins, naming, performance). |
| `/kotlin-test` | Writing JUnit 4 + IntelliJ Platform Test Framework tests for inspections, indexes, EP impls, and pure logic. |

**How they layer:** `/intellij-plugin-dev` is the foundation (general platform
knowledge). `/kotlin-dev` adds this repo's conventions on top. `/kotlin-test`
covers tests for everything either produces.

## Temporal SDK scaffolding skills

These generate **official Temporal PHP SDK** patterns the plugin already
recognises via `TemporalClasses.kt`, the file-based indexes, and the PHP
inspections.

| Skill | Purpose |
|---|---|
| `/temporal-workflow` | Create a `#[WorkflowInterface]` + implementation pair |
| `/temporal-activity` | Create an `#[ActivityInterface]` + implementation pair |
| `/temporal-signal` | Add a `#[SignalMethod]` handler to a workflow |
| `/temporal-query` | Add a `#[QueryMethod]` handler to a workflow |
| `/temporal-update` | Add an `#[UpdateMethod]` (+ optional validator) to a workflow |
| `/temporal-child-workflow` | Invoke a child workflow with `ChildWorkflowOptions` |
| `/temporal-saga` | Generate a Saga / compensation scaffold |
| `/temporal-schedule` | Create a Temporal Schedule (cron/interval) via `ScheduleClient` |
| `/temporal-worker` | Generate `worker.php` + `.rr.yaml` worker bootstrap |
| `/temporal-starter` | Generate a client script that starts a workflow |
| `/temporal-test` | Scaffold PHPUnit tests using `WorkflowEnvironment` / `ActivityMocker` |

Each skill lives under `.claude/skills/<name>/SKILL.md` and contains the full
prompt, the code template, and the conventions Claude must follow.
72 changes: 72 additions & 0 deletions .claude/skills/intellij-plugin-dev/SKILL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
---
name: intellij-plugin-dev
description: Develop IntelliJ Platform plugins (IDEA / PhpStorm / etc.) following the official JetBrains SDK guidelines. Use when the user asks to add an extension point, service, action, listener, inspection, index, tool window, notification, run configuration, or any other platform-level plugin feature. This is the general platform skill — for this project's specific conventions also consult /kotlin-dev.
---

# IntelliJ Platform Plugin Developer

Sourced from the official JetBrains SDK docs
(<https://plugins.jetbrains.com/docs/intellij/>). Use this skill as the
foundation; layer `/kotlin-dev` on top for this repo's conventions and
`/kotlin-test` for tests.

## Decision tree — what kind of feature are you adding?

| I want to... | Open |
|---|---|
| Set up / modify `plugin.xml`, module structure, resources | `structure.md` |
| Add or consume an Extension Point | `extensions.md` |
| Expose shared state / long-lived logic | `services.md` |
| Run on EDT / background; read/write PSI safely | `threading.md` |
| Walk / analyze source code | `psi.md` |
| Look up symbols fast across the project | `indexes.md` |
| Surface code warnings + quick fixes | `inspections.md` |
| Add menu items, tool windows, notifications, listeners | `actions-ui.md` |
| Change Gradle / build / runIde / verifyPlugin | `gradle.md` |
| Kotlin-specific pitfalls and idioms | `kotlin.md` |

Each file is short and self-contained — load only the one(s) you need.

## Ten non-negotiable platform rules

1. **Never block the EDT.** Long work → `Task.Backgroundable`, coroutines, or
`NonBlockingReadAction`.
2. **Mutate PSI / VFS inside a `WriteAction` on the EDT only.** Reads need a
`ReadAction` from background threads.
3. **Get services on-demand; never cache them in fields** —
`service<MyService>()` / `project.service<MyService>()`.
4. **Register listeners declaratively** in `plugin.xml`
(`<applicationListeners>`, `<projectListeners>`) — they're created lazily.
5. **Iterate EPs lazily** — `EP.lazyDumbAwareExtensions(project)`, not
`extensionList`, unless you truly need all extensions eagerly.
6. **Mark long-computation extensions as `DumbAware`** when they don't need
indexes; otherwise gate them behind `DumbService.isDumb(project)`.
7. **Cache PSI-derived values** with `CachedValuesManager` + a
`PsiModificationTracker`. Don't hand-roll memoization.
8. **Every user-visible string** comes from a `<resource-bundle>` — no inline
English.
9. **Bump `getVersion()`** on any index whose key/value layout changes —
otherwise stale data persists across upgrades.
10. **Inspections ship with an HTML description** at
`inspectionDescriptions/<shortName>.html`, or they won't pass verification.

## Typical workflow for a new feature

1. Decide scope — application / project / module / per-PSI-element.
2. Pick the right mechanism (service vs. EP vs. listener vs. action).
3. Declare it in `plugin.xml` (or the right `config-file` for an optional
dependency).
4. Implement in Kotlin. Keep classes `final` unless you *need* extensibility.
5. Wire i18n via `TemporalBundle.message("key")`.
6. Run `./gradlew build runIde verifyPlugin` — all three must pass.
7. Add tests per `/kotlin-test`.

## Reference links (official)

- Welcome: <https://plugins.jetbrains.com/docs/intellij/welcome.html>
- Plugin structure: <https://plugins.jetbrains.com/docs/intellij/plugin-structure.html>
- Threading: <https://plugins.jetbrains.com/docs/intellij/threading-model.html>
- PSI: <https://plugins.jetbrains.com/docs/intellij/psi.html>
- Indexes: <https://plugins.jetbrains.com/docs/intellij/indexing-and-psi-stubs.html>
- Kotlin for plugins: <https://plugins.jetbrains.com/docs/intellij/using-kotlin.html>
- IntelliJ Platform Gradle Plugin (v2): <https://plugins.jetbrains.com/docs/intellij/tools-intellij-platform-gradle-plugin.html>
151 changes: 151 additions & 0 deletions .claude/skills/intellij-plugin-dev/actions-ui.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,151 @@
# Actions, listeners, notifications, tool windows

## Actions

Official docs: <https://plugins.jetbrains.com/docs/intellij/basic-action-system.html>

```kotlin
class RefreshPageAction : AnAction() {
override fun getActionUpdateThread() = ActionUpdateThread.BGT

override fun update(e: AnActionEvent) {
e.presentation.isEnabled = e.project != null
}

override fun actionPerformed(e: AnActionEvent) {
val project = e.project ?: return
project.service<TemporalWebUIPanel>().reload()
}
}
```

Registration:

```xml
<actions>
<action id="Temporal.RefreshPage"
class="com.example.my.RefreshPageAction"
text="Refresh" description="Refresh the Temporal UI"
icon="AllIcons.Actions.Refresh">
<add-to-group group-id="ToolWindowContextMenu" anchor="last"/>
<keyboard-shortcut keymap="$default" first-keystroke="ctrl alt R"/>
</action>
</actions>
```

Rules:
- **Always override `getActionUpdateThread()`** — pick `BGT` when `update()`
reads project/PSI state; `EDT` only for pure UI checks. Using BGT avoids
freezes.
- `update()` must be fast. Offload work to `actionPerformed`.
- Define a `groupId` via `<group id="..." class="com.intellij.openapi.actionSystem.DefaultActionGroup">`
if you need a submenu; add actions to it with `<add-to-group>`.

## Listeners & MessageBus

Official docs: <https://plugins.jetbrains.com/docs/intellij/plugin-listeners.html>

Declarative (preferred — lazy, no startup cost):

```xml
<applicationListeners>
<listener class="com.example.my.MyAppListener"
topic="com.intellij.openapi.application.ApplicationActivationListener"/>
</applicationListeners>

<projectListeners>
<listener class="com.example.my.MyProjectListener"
topic="com.intellij.openapi.vfs.newvfs.BulkFileListener"/>
</projectListeners>
```

Programmatic:

```kotlin
project.messageBus.connect(parentDisposable)
.subscribe(BulkFileListener.TOPIC, object : BulkFileListener {
override fun after(events: MutableList<out VFileEvent>) { /* ... */ }
})
```

Custom topics:

```kotlin
interface ServerListener {
fun onServerStarted(event: ServerStarted)
companion object {
@Topic.ProjectLevel
val TOPIC: Topic<ServerListener> = Topic.create("Temporal server", ServerListener::class.java)
}
}
```

Rules:
- Listener implementations must be **stateless**; persist state in services.
- Always `connect(parentDisposable)` programmatically — orphan connections
leak.

## Notifications

```xml
<extensions defaultExtensionNs="com.intellij">
<notificationGroup id="Temporal"
displayType="BALLOON"
isLogByDefault="true"
bundle="messages.TemporalBundle"
key="notification.group"/>
</extensions>
```

`displayType`: `BALLOON` (transient popup), `STICKY_BALLOON` (stays until
dismissed), `TOOL_WINDOW` (shown inside the target tool window, needs
`toolWindowId=`), `NONE` (event log only).

Emit:

```kotlin
NotificationGroupManager.getInstance()
.getNotificationGroup("Temporal")
.createNotification(
TemporalBundle.message("notification.server.started.title"),
TemporalBundle.message("notification.server.started.content", port),
NotificationType.INFORMATION,
)
.notify(project)
```

## Tool windows

Official docs: <https://plugins.jetbrains.com/docs/intellij/tool-windows.html>

```xml
<toolWindow id="Temporal"
icon="/icons/temporal/icon.svg"
anchor="right"
factoryClass="com.example.my.TemporalWindowFactory"
doNotActivateOnStart="true"
secondary="false"/>
```

```kotlin
class TemporalWindowFactory : ToolWindowFactory, DumbAware {
override fun createToolWindowContent(project: Project, toolWindow: ToolWindow) {
val panel = TemporalWebUIPanel(project)
val content = toolWindow.contentManager.factory.createContent(panel, "Web UI", false)
toolWindow.contentManager.addContent(content)
}

override fun isApplicable(project: Project) = true // gate visibility
}
```

Retrieve at runtime:

```kotlin
ToolWindowManager.getInstance(project).getToolWindow("Temporal")?.show()
```

Rules:
- Implement `DumbAware` unless the tool window genuinely needs indexes.
- `createToolWindowContent` runs on EDT — keep it cheap; defer heavy UI wiring
to a background task or to a button click inside the panel.
84 changes: 84 additions & 0 deletions .claude/skills/intellij-plugin-dev/extensions.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
# Extension points & extensions

Official docs: <https://plugins.jetbrains.com/docs/intellij/plugin-extensions.html>

## Declaring your own EP

```xml
<extensionPoints>
<extensionPoint name="activity" dynamic="true"
interface="com.example.my.extensionPoints.Activity"/>

<!-- bean-class variant (XML-configurable) -->
<extensionPoint name="frameworkSupport" beanClass="com.intellij.util.xmlb.BaseKeyedLazyInstance">
<with attribute="implementationClass" implements="com.example.my.FrameworkSupport"/>
</extensionPoint>
</extensionPoints>
```

Guidelines:

- `name` must be unique **within your plugin**; the full ID becomes
`<pluginId>.<name>`.
- `dynamic="true"` allows the EP to be loaded/unloaded at runtime (required
for v2 plugins that support hot reload).
- Choose `interface` for code-driven extensions, `beanClass` for XML-configured
extensions with attributes.

## Consuming an EP in code

```kotlin
interface Activity {
fun getActivities(project: Project): List<ActivityModel>

companion object {
val EP = ExtensionPointName.create<Activity>("com.example.my.activity")

fun all(project: Project): List<ActivityModel> =
EP.lazyDumbAwareExtensions(project)
.flatMap { it.getActivities(project) }
.toList()
}
}
```

- Prefer `lazyDumbAwareExtensions(project)` — returns a lazy `Sequence` that
skips non-`DumbAware` extensions while indexing is in progress.
- `extensionList` / `extensions` eagerly instantiate **all** extensions — use
only if you actually need every one, and only from background threads.
- Extensions throwing `ExtensionNotApplicableException` in their constructor
are silently skipped. Use this to opt out based on runtime state.

## Contributing to someone's EP

```xml
<extensions defaultExtensionNs="com.intellij">
<localInspection implementationClass="com.example.my.MyInspection" .../>
<applicationService serviceImplementation="com.example.my.MyService"/>
</extensions>

<!-- Contributing to YOUR OWN EP -->
<extensions defaultExtensionNs="com.example.my">
<activity implementation="com.example.my.PhpActivity"/>
</extensions>
```

`defaultExtensionNs` is the plugin ID that **owns** the EP, not the caller.

## Rules for extension implementations

- **Stateless.** Per-instance mutable state breaks with dynamic reloading.
Store runtime state in a service instead.
- **Cheap constructors.** No I/O, no heavy initialization. Defer work to the
first real call.
- **No `object` singletons** in Kotlin for extension classes — the platform
instantiates them itself.
- **Thread-safety.** Extensions are called from many threads; assume concurrent
invocation.

## Optional-dependency pattern

1. `<depends optional="true" config-file="x.xml">other.plugin</depends>`.
2. Put all `<extensions>` that rely on `other.plugin`'s APIs inside `x.xml`.
3. If `other.plugin` is absent, `x.xml` is simply not loaded — the base plugin
keeps working with reduced features.
Loading
Loading