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
22 changes: 22 additions & 0 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
name: Plugin Tests
on:
pull_request:
push:
branches: [main]

jobs:
plugin-tests:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Run tests
# Auto-discovers and runs all test-*.sh scripts under any tests/ directory.
# To add a new test, just drop a test-*.sh file in a tests/ folder.
run: |
shopt -s globstar nullglob
failed=0
for f in **/tests/test-*.sh; do
echo "--- $f ---"
bash "$f" || failed=1
done
exit $failed
76 changes: 76 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
# Gemini CLI + Warp

Official [Warp](https://warp.dev) terminal integration for [Gemini CLI](https://github.com/google-gemini/gemini-cli).

## Features

### 🔔 Native Notifications

Get native Warp notifications when Gemini CLI:
- **Completes a task** — with a summary showing your prompt and Gemini's response
- **Needs your input** — when Gemini has been idle and is waiting for you
- **Requests permission** — when Gemini wants to run a tool and needs your approval

Notifications appear in Warp's notification center and as system notifications, so you can context-switch while Gemini works and get alerted when attention is needed.

### 📡 Session Status

The extension keeps Warp informed of Gemini's current state by emitting structured events on every session transition:
- **Prompt submitted** — you sent a prompt, Gemini is working
- **Tool completed** — a tool call finished, Gemini is back to running

This powers Warp's inline status indicators for Gemini CLI sessions.

## Installation

```bash
gemini extensions install <github-url-or-local-path>
```

For local development:

```bash
gemini extensions link ~/gemini-warp
```

> ⚠️ **Important**: After installing, **restart Gemini CLI** for the extension to activate.

Once restarted, notifications will appear automatically.

## Requirements

- [Warp terminal](https://warp.dev) (macOS, Linux, or Windows)
- [Gemini CLI](https://github.com/google-gemini/gemini-cli) v0.26.0+
- `jq` for JSON parsing (install via `brew install jq` or your package manager)

## How It Works

The extension communicates with Warp via OSC 777 escape sequences. Each hook script builds a structured JSON payload (via `build-payload.sh`) and sends it to `warp://cli-agent`, where Warp parses it to drive notifications and session UI.

Payloads include a protocol version negotiated between the extension and Warp (`min(plugin_version, warp_version)`), the session ID, working directory, and event-specific fields.

The extension registers five hooks:
- **SessionStart** — emits the extension version on startup
- **AfterAgent** — fires when Gemini finishes a turn, sends a task-complete notification with your prompt and Gemini's response
- **Notification** — handles both idle notifications and tool-permission requests (Gemini merges these into one event, dispatched by `notification_type`)
- **BeforeAgent** — fires when you submit a prompt, signaling the session is active again
- **AfterTool** — fires when a tool call completes, signaling the session is no longer blocked

## Configuration

Notifications work out of the box. To customize Warp's notification behavior (sounds, system notifications, etc.), see [Warp's notification settings](https://docs.warp.dev/features/notifications).

## Uninstall

```bash
gemini extensions uninstall warp
```

## Versioning

The extension version in `gemini-extension.json` is checked by the Warp client to detect outdated installations.
When bumping the version here, also update `MINIMUM_PLUGIN_VERSION` in the Warp client.

## License

MIT License — see [LICENSE](LICENSE) for details.
5 changes: 5 additions & 0 deletions gemini-extension.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"name": "gemini-warp",
"description": "Warp terminal integration for Gemini CLI - native notifications and session status",
"version": "1.0.0"
}
55 changes: 55 additions & 0 deletions hooks/hooks.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
{
"hooks": {
"SessionStart": [
{
"matcher": "startup",
"hooks": [
{
"type": "command",
"command": "${extensionPath}/scripts/on-session-start.sh"
}
]
}
],
"AfterAgent": [
{
"hooks": [
{
"type": "command",
"command": "${extensionPath}/scripts/on-stop.sh"
}
]
}
],
"Notification": [
{
"hooks": [
{
"type": "command",
"command": "${extensionPath}/scripts/on-notification.sh"
}
]
}
],
"BeforeAgent": [
{
"hooks": [
{
"type": "command",
"command": "${extensionPath}/scripts/on-prompt-submit.sh"
}
]
}
],
"AfterTool": [
{
"hooks": [
{
"type": "command",
"command": "${extensionPath}/scripts/on-post-tool-use.sh"
}
]
}
]
}
}
58 changes: 58 additions & 0 deletions scripts/build-payload.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
#!/bin/bash
# Builds a structured JSON notification payload for warp://cli-agent.
#
# Usage: source this file, then call build_payload with event-specific fields.
#
# Example:
# source "$(dirname "${BASH_SOURCE[0]}")/build-payload.sh"
# BODY=$(build_payload "$INPUT" "stop" \
# --arg query "$QUERY" \
# --arg response "$RESPONSE" \
# --arg transcript_path "$TRANSCRIPT_PATH")
#
# The function extracts common fields (session_id, cwd, project) from the
# hook's stdin JSON (passed as $1), then merges any extra jq args you pass.

# The current protocol version this plugin knows how to produce.
PLUGIN_CURRENT_PROTOCOL_VERSION=1

# Negotiate the protocol version with Warp.
# Uses min(plugin_current, warp_declared), falling back to 1 if Warp doesn't advertise a version.
negotiate_protocol_version() {
local warp_version="${WARP_CLI_AGENT_PROTOCOL_VERSION:-1}"
if [ "$warp_version" -lt "$PLUGIN_CURRENT_PROTOCOL_VERSION" ] 2>/dev/null; then
echo "$warp_version"
else
echo "$PLUGIN_CURRENT_PROTOCOL_VERSION"
fi
}

build_payload() {
local input="$1"
local event="$2"
shift 2

local protocol_version
protocol_version=$(negotiate_protocol_version)

# Extract common fields from the hook input
local session_id cwd project
session_id=$(echo "$input" | jq -r '.session_id // empty' 2>/dev/null)
cwd=$(echo "$input" | jq -r '.cwd // empty' 2>/dev/null)
project=""
if [ -n "$cwd" ]; then
project=$(basename "$cwd")
fi

# Build the payload: common fields + any extra args passed by the caller.
# Extra args should be jq flag pairs like: --arg key "value" or --argjson key '{"a":1}'
jq -nc \
--argjson v "$protocol_version" \
--arg agent "gemini" \
--arg event "$event" \
--arg session_id "$session_id" \
--arg cwd "$cwd" \
--arg project "$project" \
"$@" \
'{v:$v, agent:$agent, event:$event, session_id:$session_id, cwd:$cwd, project:$project} + $ARGS.named'
}
55 changes: 55 additions & 0 deletions scripts/on-notification.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
#!/bin/bash
# Hook script for Gemini CLI Notification event
# Handles both idle/input-needed notifications and ToolPermission (approval) notifications.
# (In Claude Code these are separate hooks: Notification + PermissionRequest.
# In Gemini CLI they are both sub-types of the Notification event.)

SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
source "$SCRIPT_DIR/should-use-structured.sh"

if ! should_use_structured; then
exit 0
fi

source "$SCRIPT_DIR/build-payload.sh"

# Read hook input from stdin
INPUT=$(cat)

NOTIF_TYPE=$(echo "$INPUT" | jq -r '.notification_type // "unknown"' 2>/dev/null)

case "$NOTIF_TYPE" in
ToolPermission)
# Permission request — Gemini puts tool info in .details
TOOL_NAME=$(echo "$INPUT" | jq -r '.details.rootCommand // .details.toolDisplayName // .details.toolName // .details.title // "unknown"' 2>/dev/null)
TOOL_INPUT=$(echo "$INPUT" | jq -c '.details // {}' 2>/dev/null)
[ -z "$TOOL_INPUT" ] && TOOL_INPUT='{}'

# Build a human-readable summary
TOOL_PREVIEW=$(echo "$INPUT" | jq -r '(.details | if .command then .command elif .filePath then .filePath elif .toolName then .toolName else (.title // "") end)' 2>/dev/null)
SUMMARY="Wants to run $TOOL_NAME"
if [ -n "$TOOL_PREVIEW" ] && [ "$TOOL_PREVIEW" != "$TOOL_NAME" ]; then
if [ ${#TOOL_PREVIEW} -gt 120 ]; then
TOOL_PREVIEW="${TOOL_PREVIEW:0:117}..."
fi
SUMMARY="$SUMMARY: $TOOL_PREVIEW"
fi

BODY=$(build_payload "$INPUT" "permission_request" \
--arg summary "$SUMMARY" \
--arg tool_name "$TOOL_NAME" \
--argjson tool_input "$TOOL_INPUT")

"$SCRIPT_DIR/warp-notify.sh" "warp://cli-agent" "$BODY"
;;
*)
# Generic notification (idle_prompt, etc.)
MSG=$(echo "$INPUT" | jq -r '.message // "Input needed"' 2>/dev/null)
[ -z "$MSG" ] && MSG="Input needed"

BODY=$(build_payload "$INPUT" "$NOTIF_TYPE" \
--arg summary "$MSG")

"$SCRIPT_DIR/warp-notify.sh" "warp://cli-agent" "$BODY"
;;
esac
27 changes: 27 additions & 0 deletions scripts/on-post-tool-use.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
#!/bin/bash
# Hook script for Gemini CLI AfterTool event (equivalent to Claude Code's PostToolUse)
# Sends a structured Warp notification after a tool call completes,
# transitioning the session status from Blocked back to Running.

SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
source "$SCRIPT_DIR/should-use-structured.sh"

if ! should_use_structured; then
echo '{}'
exit 0
fi

source "$SCRIPT_DIR/build-payload.sh"

# Read hook input from stdin
INPUT=$(cat)

TOOL_NAME=$(echo "$INPUT" | jq -r '.tool_name // empty' 2>/dev/null)

BODY=$(build_payload "$INPUT" "tool_complete" \
--arg tool_name "$TOOL_NAME")

"$SCRIPT_DIR/warp-notify.sh" "warp://cli-agent" "$BODY"

# Output empty JSON so we don't interfere with the agent
echo '{}'
31 changes: 31 additions & 0 deletions scripts/on-prompt-submit.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
#!/bin/bash
# Hook script for Gemini CLI BeforeAgent event (equivalent to Claude Code's UserPromptSubmit)
# Sends a structured Warp notification when the user submits a prompt,
# transitioning the session status from idle/blocked back to running.

SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
source "$SCRIPT_DIR/should-use-structured.sh"

if ! should_use_structured; then
echo '{}'
exit 0
fi

source "$SCRIPT_DIR/build-payload.sh"

# Read hook input from stdin
INPUT=$(cat)

# Extract the user's prompt
QUERY=$(echo "$INPUT" | jq -r '.prompt // empty' 2>/dev/null)
if [ -n "$QUERY" ] && [ ${#QUERY} -gt 200 ]; then
QUERY="${QUERY:0:197}..."
fi

BODY=$(build_payload "$INPUT" "prompt_submit" \
--arg query "$QUERY")

"$SCRIPT_DIR/warp-notify.sh" "warp://cli-agent" "$BODY"

# Output empty JSON so we don't interfere with the agent
echo '{}'
31 changes: 31 additions & 0 deletions scripts/on-session-start.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
#!/bin/bash
# Hook script for Gemini CLI SessionStart event
# Shows welcome message, Warp detection status, and emits plugin version

SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
source "$SCRIPT_DIR/should-use-structured.sh"

if ! should_use_structured; then
exit 0
fi

if ! command -v jq &>/dev/null; then
cat << 'EOF'
{
"systemMessage": "🚨 Warp notifications require jq! Install it with your system package manager (e.g. brew install jq, apt install jq) 🚨"
}
EOF
exit 0
fi
source "$SCRIPT_DIR/build-payload.sh"

# Read hook input from stdin
INPUT=$(cat)

# Read plugin version from gemini-extension.json
PLUGIN_VERSION=$(jq -r '.version // "unknown"' "$SCRIPT_DIR/../gemini-extension.json" 2>/dev/null)

# Emit structured notification with plugin version so Warp can track it
BODY=$(build_payload "$INPUT" "session_start" \
--arg plugin_version "$PLUGIN_VERSION")
"$SCRIPT_DIR/warp-notify.sh" "warp://cli-agent" "$BODY"
Loading
Loading