opencode-handoff 0.1.0 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +19 -0
- package/package.json +1 -1
- package/src/plugin.ts +86 -6
package/README.md
CHANGED
|
@@ -9,6 +9,7 @@ Inspired by Amp's handoff command - see their [post](https://ampcode.com/news/ha
|
|
|
9
9
|
- `/handoff <goal>` command that analyzes the conversation and generates a continuation prompt
|
|
10
10
|
- Guides the AI to include relevant `@file` references so the next session starts with context loaded
|
|
11
11
|
- Opens a new session with the prompt as an editable draft
|
|
12
|
+
- `read_session` tool for retrieving full conversation transcripts from previous sessions when the handoff summary isn't sufficient
|
|
12
13
|
|
|
13
14
|
## Requirements
|
|
14
15
|
|
|
@@ -53,6 +54,24 @@ ln -sf ~/.config/opencode/opencode-handoff/src/plugin.ts ~/.config/opencode/plug
|
|
|
53
54
|
|
|
54
55
|
The AI analyzes the conversation, extracts key decisions and relevant files, generates a focused prompt, and creates a new session with that prompt ready to edit.
|
|
55
56
|
|
|
57
|
+
### Reading Previous Session Transcripts
|
|
58
|
+
|
|
59
|
+
When you use `/handoff`, the generated prompt includes a session reference line:
|
|
60
|
+
|
|
61
|
+
```
|
|
62
|
+
Continuing work from session sess_01jxyz123. When you lack specific information you can use read_session to get it.
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
This gives the AI in the new session access to the `read_session` tool, which can fetch the full conversation transcript from the source session. If the handoff summary doesn't include something you need, just ask - the AI can look it up.
|
|
66
|
+
|
|
67
|
+
**Example:**
|
|
68
|
+
|
|
69
|
+
```
|
|
70
|
+
You: What were the specific error messages we saw earlier?
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
The AI will use `read_session` to retrieve details from the previous session that weren't included in the handoff summary.
|
|
74
|
+
|
|
56
75
|
## Contributing
|
|
57
76
|
|
|
58
77
|
Contributions are welcome! Here's how to set up for development:
|
package/package.json
CHANGED
package/src/plugin.ts
CHANGED
|
@@ -1,6 +1,46 @@
|
|
|
1
1
|
import type { Plugin } from "@opencode-ai/plugin"
|
|
2
2
|
import { tool } from "@opencode-ai/plugin"
|
|
3
3
|
|
|
4
|
+
function formatTranscript(messages: Array<{ info: any; parts: any[] }>, limit?: number): string {
|
|
5
|
+
const lines: string[] = []
|
|
6
|
+
|
|
7
|
+
for (const msg of messages) {
|
|
8
|
+
if (msg.info.role === "user") {
|
|
9
|
+
lines.push("## User")
|
|
10
|
+
for (const part of msg.parts) {
|
|
11
|
+
if (part.type === "text" && !part.ignored) {
|
|
12
|
+
lines.push(part.text)
|
|
13
|
+
}
|
|
14
|
+
if (part.type === "file") {
|
|
15
|
+
lines.push(`[Attached: ${part.filename || "file"}]`)
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
lines.push("")
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
if (msg.info.role === "assistant") {
|
|
22
|
+
lines.push("## Assistant")
|
|
23
|
+
for (const part of msg.parts) {
|
|
24
|
+
if (part.type === "text") {
|
|
25
|
+
lines.push(part.text)
|
|
26
|
+
}
|
|
27
|
+
if (part.type === "tool" && part.state.status === "completed") {
|
|
28
|
+
lines.push(`[Tool: ${part.tool}] ${part.state.title}`)
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
lines.push("")
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const output = lines.join("\n").trim()
|
|
36
|
+
|
|
37
|
+
if (messages.length >= (limit ?? 100)) {
|
|
38
|
+
return output + `\n\n(Showing ${messages.length} most recent messages. Use a higher 'limit' to see more.)`
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
return output + `\n\n(End of session - ${messages.length} messages)`
|
|
42
|
+
}
|
|
43
|
+
|
|
4
44
|
export const HandoffPlugin: Plugin = async (ctx) => ({
|
|
5
45
|
config: async (config) => {
|
|
6
46
|
config.command = config.command || {}
|
|
@@ -16,7 +56,9 @@ A good handoff frontloads everything the next session needs so it can start impl
|
|
|
16
56
|
|
|
17
57
|
Analyze this conversation and extract what matters for continuing the work.
|
|
18
58
|
|
|
19
|
-
|
|
59
|
+
## OUTPUT FORMAT
|
|
60
|
+
|
|
61
|
+
1. FILE REFERENCES
|
|
20
62
|
|
|
21
63
|
Include all relevant @file references on a SINGLE LINE, space-separated.
|
|
22
64
|
|
|
@@ -36,22 +78,33 @@ The user controls what context matters. If they mentioned something to preserve,
|
|
|
36
78
|
|
|
37
79
|
---
|
|
38
80
|
|
|
39
|
-
After generating the handoff message, IMMEDIATELY call
|
|
40
|
-
\`
|
|
81
|
+
After generating the handoff message, IMMEDIATELY call handoff_session with the full message as a handoff prompt:
|
|
82
|
+
\`handoff_session(prompt="...")\``,
|
|
41
83
|
}
|
|
42
84
|
},
|
|
43
85
|
|
|
44
86
|
tool: {
|
|
45
|
-
|
|
46
|
-
description: "
|
|
87
|
+
handoff_session: tool({
|
|
88
|
+
description: "Create a new session with the handoff prompt as an editable draft",
|
|
47
89
|
args: {
|
|
48
90
|
prompt: tool.schema.string().describe("The generated handoff prompt"),
|
|
49
91
|
},
|
|
50
92
|
async execute(args, context) {
|
|
93
|
+
// Capture current session ID before switching to new session
|
|
94
|
+
const sourceSessionID = context.sessionID
|
|
95
|
+
const sessionReference = `Continuing work from session ${sourceSessionID}. When you lack specific information you can use read_session to get it.`
|
|
96
|
+
const fullPrompt = `${sessionReference}\n\n${args.prompt}`
|
|
97
|
+
|
|
98
|
+
// Double-append workaround for textarea resize bug:
|
|
99
|
+
// appendPrompt uses insertText() which bypasses onContentChange, so resize never triggers.
|
|
100
|
+
// First append sets height in old session, session_new preserves textarea element,
|
|
101
|
+
// second append populates new session with already-expanded textarea.
|
|
51
102
|
await ctx.client.tui.clearPrompt()
|
|
103
|
+
await new Promise(r => setTimeout(r, 200))
|
|
104
|
+
await ctx.client.tui.appendPrompt({ body: { text: fullPrompt } })
|
|
52
105
|
await ctx.client.tui.executeCommand({ body: { command: "session_new" } })
|
|
53
106
|
await new Promise(r => setTimeout(r, 200))
|
|
54
|
-
await ctx.client.tui.appendPrompt({ body: { text:
|
|
107
|
+
await ctx.client.tui.appendPrompt({ body: { text: fullPrompt } })
|
|
55
108
|
|
|
56
109
|
await ctx.client.tui.showToast({
|
|
57
110
|
body: {
|
|
@@ -64,6 +117,33 @@ After generating the handoff message, IMMEDIATELY call handoff_prepare with the
|
|
|
64
117
|
|
|
65
118
|
return "Handoff prompt created in new session. Review and edit before sending."
|
|
66
119
|
}
|
|
120
|
+
}),
|
|
121
|
+
|
|
122
|
+
read_session: tool({
|
|
123
|
+
description: "Read the conversation transcript from a previous session. Use this when you need specific information from the source session that wasn't included in the handoff summary.",
|
|
124
|
+
args: {
|
|
125
|
+
sessionID: tool.schema.string().describe("The full session ID (e.g., sess_01jxyz...)"),
|
|
126
|
+
limit: tool.schema.number().optional().describe("Maximum number of messages to read (defaults to 100, max 500)"),
|
|
127
|
+
},
|
|
128
|
+
async execute(args, context) {
|
|
129
|
+
const limit = Math.min(args.limit ?? 100, 500)
|
|
130
|
+
|
|
131
|
+
try {
|
|
132
|
+
const response = await ctx.client.session.messages({
|
|
133
|
+
path: { id: args.sessionID },
|
|
134
|
+
query: { limit }
|
|
135
|
+
})
|
|
136
|
+
|
|
137
|
+
if (!response.data || response.data.length === 0) {
|
|
138
|
+
return "Session has no messages or does not exist."
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
const formatted = formatTranscript(response.data, limit)
|
|
142
|
+
return formatted
|
|
143
|
+
} catch (error) {
|
|
144
|
+
return `Could not read session ${args.sessionID}: ${error instanceof Error ? error.message : 'Unknown error'}`
|
|
145
|
+
}
|
|
146
|
+
}
|
|
67
147
|
})
|
|
68
148
|
}
|
|
69
149
|
})
|