opencode-plannotator-tui 0.2.1 → 0.2.2
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/CHANGELOG.md +5 -0
- package/README.md +3 -3
- package/package.json +1 -1
- package/review.mjs +26 -26
package/CHANGELOG.md
CHANGED
package/README.md
CHANGED
|
@@ -61,8 +61,8 @@ Manual configuration, preserving other entries:
|
|
|
61
61
|
3. Press `c` to comment, `a` for “looks good,” or `d` to request deletion.
|
|
62
62
|
Without a selection, `c` comments on the current block. Enter saves a comment.
|
|
63
63
|
4. Press **`q`** to return to OpenCode.
|
|
64
|
-
5. Choose
|
|
65
|
-
|
|
64
|
+
5. Choose **Send and continue** to submit the annotations. Escape or **Cancel**
|
|
65
|
+
cancels.
|
|
66
66
|
|
|
67
67
|
The upstream **`E`** action copies to the clipboard. This adapter's automatic
|
|
68
68
|
delivery path is **q → confirm** and does not need clipboard access.
|
|
@@ -162,7 +162,7 @@ See [release instructions](docs/releasing.md) and [changelog](CHANGELOG.md).
|
|
|
162
162
|
- `/annotate`:批注上一条完整回复。
|
|
163
163
|
- `/annotate 文档.md`:批注文档,支持中文、空格和相对路径。
|
|
164
164
|
- `/annotate-last`:同义入口,也可带文件路径。
|
|
165
|
-
- 选中文字 → `c` 写批注 → Enter 保存 → `q` 返回
|
|
165
|
+
- 选中文字 → `c` 写批注 → Enter 保存 → `q` 返回 →「Send and continue」。
|
|
166
166
|
- 文档采用独立快照,反馈附原文件路径,确认后发回发起批注的会话。
|
|
167
167
|
- 更新:`opencode plugin update opencode-plannotator-tui@latest`,然后重新打开终端客户端。
|
|
168
168
|
|
package/package.json
CHANGED
package/review.mjs
CHANGED
|
@@ -15,26 +15,26 @@ async function documentSource(input, client, sessionID, signal) {
|
|
|
15
15
|
target = target.slice(1, -1)
|
|
16
16
|
}
|
|
17
17
|
if (!target || !documentExtensions.has(path.extname(target).toLowerCase())) {
|
|
18
|
-
throw new Error("
|
|
18
|
+
throw new Error("Choose a Markdown or plain-text file (.md, .mdx, .markdown, .txt).")
|
|
19
19
|
}
|
|
20
|
-
if (/^[a-z][a-z0-9+.-]*:\/\//i.test(target)) throw new Error("
|
|
20
|
+
if (/^[a-z][a-z0-9+.-]*:\/\//i.test(target)) throw new Error("Use a local file path.")
|
|
21
21
|
if (target.startsWith("~/")) target = path.join(os.homedir(), target.slice(2))
|
|
22
22
|
const session = await client.session.get({ sessionID }, { signal })
|
|
23
23
|
const directory = session.location?.directory
|
|
24
|
-
if (!path.isAbsolute(target) && !directory) throw new Error("
|
|
24
|
+
if (!path.isAbsolute(target) && !directory) throw new Error("Can't determine the current session's working directory.")
|
|
25
25
|
const filePath = path.isAbsolute(target) ? path.normalize(target) : path.resolve(directory, target)
|
|
26
26
|
try {
|
|
27
27
|
const info = await stat(filePath)
|
|
28
|
-
if (!info.isFile()) throw new Error("
|
|
29
|
-
if (info.size > maxDocumentBytes) throw new Error("
|
|
28
|
+
if (!info.isFile()) throw new Error("The target is not a file.")
|
|
29
|
+
if (info.size > maxDocumentBytes) throw new Error("The file is larger than 2 MiB. Choose a smaller document.")
|
|
30
30
|
const bytes = await readFile(filePath, { signal })
|
|
31
|
-
if (bytes.length > maxDocumentBytes) throw new Error("
|
|
32
|
-
if (bytes.includes(0)) throw new Error("
|
|
31
|
+
if (bytes.length > maxDocumentBytes) throw new Error("The file is larger than 2 MiB.")
|
|
32
|
+
if (bytes.includes(0)) throw new Error("The file contains binary content.")
|
|
33
33
|
let text
|
|
34
34
|
try {
|
|
35
35
|
text = new TextDecoder("utf-8", { fatal: true }).decode(bytes)
|
|
36
36
|
} catch {
|
|
37
|
-
throw new Error("
|
|
37
|
+
throw new Error("Use a UTF-8 text file.")
|
|
38
38
|
}
|
|
39
39
|
return {
|
|
40
40
|
text, filePath,
|
|
@@ -43,7 +43,7 @@ async function documentSource(input, client, sessionID, signal) {
|
|
|
43
43
|
}
|
|
44
44
|
} catch (error) {
|
|
45
45
|
if (signal.aborted) throw error
|
|
46
|
-
throw new Error(
|
|
46
|
+
throw new Error(`Can't read document "${filePath}": ${error.code === "ENOENT" ? "file not found." : error.message}`)
|
|
47
47
|
}
|
|
48
48
|
}
|
|
49
49
|
|
|
@@ -71,7 +71,7 @@ async function latestReply(client, sessionID, signal) {
|
|
|
71
71
|
const reply = selectReply(page.data)
|
|
72
72
|
if (reply) return reply
|
|
73
73
|
cursor = page.cursor?.next
|
|
74
|
-
if (cursor && seen.has(cursor)) throw new Error("
|
|
74
|
+
if (cursor && seen.has(cursor)) throw new Error("Session history returned a repeated page cursor.")
|
|
75
75
|
seen.add(cursor)
|
|
76
76
|
} while (cursor)
|
|
77
77
|
}
|
|
@@ -88,7 +88,7 @@ async function archivedNotes(data) {
|
|
|
88
88
|
|
|
89
89
|
async function interactive({ binary, file, cwd, env, signal }) {
|
|
90
90
|
if (!process.stdin.isTTY || !process.stdout.isTTY) {
|
|
91
|
-
throw new Error("
|
|
91
|
+
throw new Error("Run /annotate in the full OpenCode terminal interface.")
|
|
92
92
|
}
|
|
93
93
|
await new Promise((resolve, reject) => {
|
|
94
94
|
const child = spawn(binary, [file], { cwd, env, stdio: "inherit", signal })
|
|
@@ -111,10 +111,10 @@ export function createReviewCommand(context, options = {}) {
|
|
|
111
111
|
|
|
112
112
|
async function run(input = "") {
|
|
113
113
|
if (disposed) return
|
|
114
|
-
if (active) return notice("
|
|
114
|
+
if (active) return notice("An annotation is already in progress.")
|
|
115
115
|
// Capture before the first await: changing tabs must never redirect a review.
|
|
116
116
|
const route = context.ui.router.current()
|
|
117
|
-
if (route.type !== "session") return notice("
|
|
117
|
+
if (route.type !== "session") return notice("Open a session first.")
|
|
118
118
|
const sessionID = route.sessionID
|
|
119
119
|
const title = context.data.session.get(sessionID)?.title ?? sessionID
|
|
120
120
|
const controller = new AbortController()
|
|
@@ -129,7 +129,7 @@ export function createReviewCommand(context, options = {}) {
|
|
|
129
129
|
? await documentSource(target, context.client, sessionID, controller.signal)
|
|
130
130
|
: await latestReply(context.client, sessionID, controller.signal)
|
|
131
131
|
if (controller.signal.aborted) return
|
|
132
|
-
if (!source) return notice("
|
|
132
|
+
if (!source) return notice("This session has no completed assistant reply yet.")
|
|
133
133
|
|
|
134
134
|
directory = await mkdtemp(path.join(options.tempRoot ?? os.tmpdir(), "opencode-annotate-"))
|
|
135
135
|
keepDraft = true
|
|
@@ -171,42 +171,42 @@ export function createReviewCommand(context, options = {}) {
|
|
|
171
171
|
})
|
|
172
172
|
const feedback = stdout.trim()
|
|
173
173
|
const archived = await archivedNotes(data)
|
|
174
|
-
const retainedArchive = archived ? `\n${archived}
|
|
174
|
+
const retainedArchive = archived ? `\n${archived} archived ${archived === 1 ? "note was" : "notes were"} kept in ${directory}.` : ""
|
|
175
175
|
// Upstream 0.9.4 emits this sentinel rather than empty stdout.
|
|
176
176
|
if (!feedback || feedback === "No annotations.") {
|
|
177
177
|
keepDraft = archived > 0
|
|
178
|
-
return notice(archived ?
|
|
178
|
+
return notice(archived ? `No annotations to send.${retainedArchive}` : "No annotations. Returned to the session.", archived ? "warning" : "info")
|
|
179
179
|
}
|
|
180
|
-
if (!feedback.startsWith("# Annotations on ")) throw new Error("
|
|
180
|
+
if (!feedback.startsWith("# Annotations on ")) throw new Error("Can't recognize the TUI annotation export format.")
|
|
181
181
|
const subject = source.filePath
|
|
182
|
-
?
|
|
183
|
-
:
|
|
182
|
+
? `Here are my annotations on the document ${JSON.stringify(source.filePath)}. Please apply the feedback to that file.\nThe annotations are based on a snapshot taken when the document was opened (SHA-256: ${source.sourceHash}). Re-read the file and check the quoted text before editing.`
|
|
183
|
+
: `Here are my annotations on your reply ${source.id}. Please address the feedback.`
|
|
184
184
|
const text = `${subject}\n\n${feedback}`
|
|
185
185
|
const feedbackPath = path.join(directory, "feedback.md")
|
|
186
186
|
await writeFile(feedbackPath, text, { mode: 0o600 })
|
|
187
187
|
const count = (feedback.match(/^## Annotation /gm) ?? []).length
|
|
188
188
|
const confirmed = await context.ui.dialog.confirm({
|
|
189
|
-
title: "
|
|
190
|
-
message: `${source.filePath ?
|
|
191
|
-
label: { confirm: "
|
|
189
|
+
title: "Send annotations?",
|
|
190
|
+
message: `${source.filePath ? `Document: ${source.filePath}\n` : ""}Send ${count} ${count === 1 ? "annotation" : "annotations"} to "${title}" and let the assistant continue?`,
|
|
191
|
+
label: { confirm: "Send and continue", cancel: "Cancel" },
|
|
192
192
|
})
|
|
193
193
|
if (controller.signal.aborted) return
|
|
194
194
|
if (confirmed !== true) {
|
|
195
195
|
keepDraft = archived > 0
|
|
196
|
-
return notice(
|
|
196
|
+
return notice(`Sending cancelled.${retainedArchive}`)
|
|
197
197
|
}
|
|
198
198
|
await context.client.session.prompt({
|
|
199
199
|
sessionID, id: promptID, text, delivery: "queue", resume: true,
|
|
200
200
|
})
|
|
201
201
|
keepDraft = archived > 0
|
|
202
|
-
notice(
|
|
202
|
+
notice(`Sent ${count} ${count === 1 ? "annotation" : "annotations"}.${retainedArchive}`, "success")
|
|
203
203
|
} catch (error) {
|
|
204
204
|
if (!disposed) {
|
|
205
205
|
const detail = error?.code === "ENOENT"
|
|
206
|
-
? "
|
|
206
|
+
? "Can't find plannotator-tui. Check that it is installed and on PATH."
|
|
207
207
|
: error instanceof Error ? error.message : String(error)
|
|
208
208
|
const recovery = keepDraft && directory
|
|
209
|
-
? `\
|
|
209
|
+
? `\nThe draft was kept in ${directory}. If it was exported, the feedback is in feedback.md.`
|
|
210
210
|
: ""
|
|
211
211
|
notice(`${detail}${recovery}`, "error")
|
|
212
212
|
}
|