Skip to content

Commit 80e7c8b

Browse files
committed
clean up plugin
1 parent 0abfa16 commit 80e7c8b

File tree

3 files changed

+121
-126
lines changed

3 files changed

+121
-126
lines changed

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@ This plugin uses Warp's [pluggable notifications](https://docs.warp.dev/features
5454
The plugin hooks into these OpenCode events:
5555
- **session.created** — confirms the plugin is active
5656
- **session.idle** — fires when OpenCode finishes responding, includes your prompt and the response
57-
- **permission.asked** — fires when OpenCode needs tool approval
57+
- **permission.updated** / **permission.asked** — fires when OpenCode needs tool approval
5858
- **message.updated** — fires when a user prompt is submitted
5959
- **tool.execute.after** — fires when a tool call completes
6060

package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
"scripts": {
1111
"build": "tsc",
1212
"typecheck": "tsc --noEmit",
13-
"test": "npx tsx --test tests/payload.test.ts",
13+
"test": "bun test",
1414
"dev": "echo 'Add to your opencode.json: \"plugin\": [\"file://'$(pwd)'/src/index.ts\"]' && echo 'Then run: opencode'"
1515
},
1616
"keywords": [
@@ -24,6 +24,7 @@
2424
"name": "Warp",
2525
"url": "https://warp.dev"
2626
},
27+
"type": "module",
2728
"license": "MIT",
2829
"homepage": "https://github.com/warpdotdev/opencode-warp",
2930
"repository": {

src/index.ts

Lines changed: 118 additions & 124 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,5 @@
11
import type { Plugin } from "@opencode-ai/plugin"
2-
import type {
3-
Event,
4-
EventSessionCreated,
5-
EventSessionIdle,
6-
EventMessageUpdated,
7-
EventPermissionUpdated,
8-
} from "@opencode-ai/sdk"
2+
import type { Event, Part, Permission } from "@opencode-ai/sdk"
93

104
import { buildPayload } from "./payload"
115
import { warpNotify } from "./notify"
@@ -18,15 +12,45 @@ function truncate(str: string, maxLen: number): string {
1812
return str.slice(0, maxLen - 3) + "..."
1913
}
2014

21-
function extractTextFromParts(
22-
parts: Array<{ type?: string; text?: string }>,
23-
): string {
15+
function extractTextFromParts(parts: Part[]): string {
2416
return parts
25-
.filter((p) => p.type === "text" && p.text)
26-
.map((p) => p.text!)
17+
.filter((p): p is Part & { type: "text"; text: string } =>
18+
p.type === "text" && "text" in p && Boolean(p.text),
19+
)
20+
.map((p) => p.text)
2721
.join(" ")
2822
}
2923

24+
function sendPermissionNotification(perm: Permission, cwd: string): void {
25+
const sessionId = perm.sessionID
26+
const toolName = perm.type || "unknown"
27+
const metadata = perm.metadata || {}
28+
29+
let toolPreview = ""
30+
if (typeof metadata.command === "string") {
31+
toolPreview = metadata.command
32+
} else if (typeof metadata.file_path === "string") {
33+
toolPreview = metadata.file_path as string
34+
} else if (typeof metadata.filePath === "string") {
35+
toolPreview = metadata.filePath as string
36+
} else {
37+
const raw = JSON.stringify(metadata)
38+
toolPreview = raw.slice(0, 80)
39+
}
40+
41+
let summary = `Wants to run ${toolName}`
42+
if (toolPreview) {
43+
summary += `: ${truncate(toolPreview, 120)}`
44+
}
45+
46+
const body = buildPayload("permission_request", sessionId, cwd, {
47+
summary,
48+
tool_name: toolName,
49+
tool_input: metadata,
50+
})
51+
warpNotify(NOTIFICATION_TITLE, body)
52+
}
53+
3054
export const WarpPlugin: Plugin = async ({ client, directory }) => {
3155
await client.app.log({
3256
body: {
@@ -40,133 +64,103 @@ export const WarpPlugin: Plugin = async ({ client, directory }) => {
4064
event: async ({ event }: { event: Event }) => {
4165
const cwd = directory || ""
4266

43-
if (event.type === "session.created") {
44-
const ev = event as EventSessionCreated
45-
const sessionId = ev.properties.info.id
46-
const body = buildPayload("session_start", sessionId, cwd, {
47-
plugin_version: PLUGIN_VERSION,
48-
})
49-
warpNotify(NOTIFICATION_TITLE, body)
50-
return
51-
}
52-
53-
if (event.type === "session.idle") {
54-
const ev = event as EventSessionIdle
55-
const sessionId = ev.properties.sessionID
56-
57-
// Fetch the conversation to extract last query and response
58-
// (port of on-stop.sh transcript parsing)
59-
let query = ""
60-
let response = ""
61-
62-
if (sessionId) {
63-
try {
64-
const result = await client.session.messages({
65-
path: { id: sessionId },
66-
})
67-
const messages = result.data as
68-
| Array<{
69-
info: { role?: string }
70-
parts: Array<{ type?: string; text?: string }>
71-
}>
72-
| undefined
73-
74-
if (messages) {
75-
const lastUser = [...messages]
76-
.reverse()
77-
.find((m) => m.info.role === "user")
78-
if (lastUser) {
79-
query = extractTextFromParts(lastUser.parts)
80-
}
67+
switch (event.type) {
68+
case "session.created": {
69+
const sessionId = event.properties.info.id
70+
const body = buildPayload("session_start", sessionId, cwd, {
71+
plugin_version: PLUGIN_VERSION,
72+
})
73+
warpNotify(NOTIFICATION_TITLE, body)
74+
return
75+
}
8176

82-
const lastAssistant = [...messages]
83-
.reverse()
84-
.find((m) => m.info.role === "assistant")
85-
if (lastAssistant) {
86-
response = extractTextFromParts(lastAssistant.parts)
77+
case "session.idle": {
78+
const sessionId = event.properties.sessionID
79+
80+
// Fetch the conversation to extract last query and response
81+
// (port of on-stop.sh transcript parsing)
82+
let query = ""
83+
let response = ""
84+
85+
if (sessionId) {
86+
try {
87+
const result = await client.session.messages({
88+
path: { id: sessionId },
89+
})
90+
const messages = result.data
91+
92+
if (messages) {
93+
const reversed = [...messages].reverse()
94+
95+
const lastUser = reversed.find(
96+
(m) => m.info.role === "user",
97+
)
98+
if (lastUser) {
99+
query = extractTextFromParts(lastUser.parts)
100+
}
101+
102+
const lastAssistant = reversed.find(
103+
(m) => m.info.role === "assistant",
104+
)
105+
if (lastAssistant) {
106+
response = extractTextFromParts(lastAssistant.parts)
107+
}
87108
}
109+
} catch {
110+
// If we can't fetch messages, send the notification without query/response
88111
}
89-
} catch {
90-
// If we can't fetch messages, send the notification without query/response
91112
}
92-
}
93-
94-
const body = buildPayload("stop", sessionId, cwd, {
95-
query: truncate(query, 200),
96-
response: truncate(response, 200),
97-
transcript_path: "",
98-
})
99-
warpNotify(NOTIFICATION_TITLE, body)
100-
return
101-
}
102113

103-
if (event.type === "permission.updated" || (event as any).type === "permission.asked") {
104-
const ev = event as EventPermissionUpdated
105-
const perm = ev.properties
106-
const sessionId = perm.sessionID
107-
const toolName = perm.type || "unknown"
108-
const metadata = perm.metadata || {}
109-
110-
let toolPreview = ""
111-
if (typeof metadata.command === "string") {
112-
toolPreview = metadata.command
113-
} else if (typeof metadata.file_path === "string") {
114-
toolPreview = metadata.file_path as string
115-
} else if (typeof metadata.filePath === "string") {
116-
toolPreview = metadata.filePath as string
117-
} else {
118-
const raw = JSON.stringify(metadata)
119-
toolPreview = raw.slice(0, 80)
114+
const body = buildPayload("stop", sessionId, cwd, {
115+
query: truncate(query, 200),
116+
response: truncate(response, 200),
117+
transcript_path: "",
118+
})
119+
warpNotify(NOTIFICATION_TITLE, body)
120+
return
120121
}
121122

122-
let summary = `Wants to run ${toolName}`
123-
if (toolPreview) {
124-
summary += `: ${truncate(toolPreview, 120)}`
123+
case "permission.updated": {
124+
sendPermissionNotification(event.properties, cwd)
125+
return
125126
}
126127

127-
const body = buildPayload("permission_request", sessionId, cwd, {
128-
summary,
129-
tool_name: toolName,
130-
tool_input: metadata,
131-
})
132-
warpNotify(NOTIFICATION_TITLE, body)
133-
return
134-
}
128+
case "message.updated": {
129+
const message = event.properties.info
130+
if (message.role !== "user") return
131+
132+
const sessionId = message.sessionID
135133

136-
if (event.type === "message.updated") {
137-
const ev = event as EventMessageUpdated
138-
const message = ev.properties.info
139-
if (message.role !== "user") return
134+
// message.updated doesn't carry parts directly — fetch the message
135+
let queryText = ""
136+
try {
137+
const result = await client.session.message({
138+
path: { id: sessionId, messageID: message.id },
139+
})
140+
if (result.data) {
141+
queryText = extractTextFromParts(result.data.parts)
142+
}
143+
} catch {
144+
// Fall back to using summary title if available
145+
queryText = message.summary?.title ?? ""
146+
}
140147

141-
const sessionId = message.sessionID
148+
if (!queryText) return
142149

143-
// message.updated doesn't carry parts directly — fetch the message
144-
let queryText = ""
145-
try {
146-
const result = await client.session.message({
147-
path: { id: sessionId, messageID: message.id },
150+
const body = buildPayload("prompt_submit", sessionId, cwd, {
151+
query: truncate(queryText, 200),
148152
})
149-
const data = result.data as
150-
| {
151-
info: { role?: string }
152-
parts: Array<{ type?: string; text?: string }>
153-
}
154-
| undefined
155-
if (data) {
156-
queryText = extractTextFromParts(data.parts)
157-
}
158-
} catch {
159-
// Fall back to using summary title if available
160-
queryText = message.summary?.title ?? ""
153+
warpNotify(NOTIFICATION_TITLE, body)
154+
return
161155
}
162156

163-
if (!queryText) return
164-
165-
const body = buildPayload("prompt_submit", sessionId, cwd, {
166-
query: truncate(queryText, 200),
167-
})
168-
warpNotify(NOTIFICATION_TITLE, body)
169-
return
157+
default: {
158+
// permission.asked is listed in the opencode docs but has no SDK type.
159+
// Handle it with the same logic as permission.updated.
160+
if ((event as any).type === "permission.asked") {
161+
sendPermissionNotification((event as any).properties, cwd)
162+
}
163+
}
170164
}
171165
},
172166

0 commit comments

Comments
 (0)