opencode-summary 0.0.0-stage → 0.1.1
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/LICENSE +21 -0
- package/README.md +157 -2
- package/dist/tui.js +953 -0
- package/package.json +60 -4
- package/src/lib.ts +135 -0
- package/src/prompt.ts +12 -0
- package/src/tui.tsx +558 -0
package/package.json
CHANGED
|
@@ -1,6 +1,62 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "opencode-summary",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "0.1.1",
|
|
4
|
+
"description": "opencode TUI plugin: generate and view a summary of the current session",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"author": "frostehhh",
|
|
8
|
+
"repository": {
|
|
9
|
+
"type": "git",
|
|
10
|
+
"url": "git+https://github.com/frostehhh/opencode-summary.git"
|
|
11
|
+
},
|
|
12
|
+
"bugs": {
|
|
13
|
+
"url": "https://github.com/frostehhh/opencode-summary/issues"
|
|
14
|
+
},
|
|
15
|
+
"homepage": "https://github.com/frostehhh/opencode-summary#readme",
|
|
16
|
+
"keywords": [
|
|
17
|
+
"opencode",
|
|
18
|
+
"opencode-plugin",
|
|
19
|
+
"tui",
|
|
20
|
+
"summary"
|
|
21
|
+
],
|
|
22
|
+
"exports": {
|
|
23
|
+
"./tui": "./dist/tui.js"
|
|
24
|
+
},
|
|
25
|
+
"files": [
|
|
26
|
+
"dist",
|
|
27
|
+
"src",
|
|
28
|
+
"LICENSE",
|
|
29
|
+
"README.md"
|
|
30
|
+
],
|
|
31
|
+
"scripts": {
|
|
32
|
+
"build": "bun scripts/build.ts",
|
|
33
|
+
"test": "bun test",
|
|
34
|
+
"prepublishOnly": "bun run build && bun test",
|
|
35
|
+
"prepare": "husky"
|
|
36
|
+
},
|
|
37
|
+
"peerDependencies": {
|
|
38
|
+
"@opentui/core": "",
|
|
39
|
+
"@opentui/solid": "",
|
|
40
|
+
"solid-js": ""
|
|
41
|
+
},
|
|
42
|
+
"devDependencies": {
|
|
43
|
+
"@commitlint/cli": "^21.2.4",
|
|
44
|
+
"@commitlint/config-conventional": "^21.2.4",
|
|
45
|
+
"@opencode/plugin": "^2.2.6",
|
|
46
|
+
"@opentui/core": "^0.8.3",
|
|
47
|
+
"@opentui/solid": "^0.8.3",
|
|
48
|
+
"husky": "^9.1.7",
|
|
49
|
+
"solid-js": "^1.9.18"
|
|
50
|
+
},
|
|
51
|
+
"peerDependenciesMeta": {
|
|
52
|
+
"@opentui/core": {
|
|
53
|
+
"optional": true
|
|
54
|
+
},
|
|
55
|
+
"@opentui/solid": {
|
|
56
|
+
"optional": true
|
|
57
|
+
},
|
|
58
|
+
"solid-js": {
|
|
59
|
+
"optional": true
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
}
|
package/src/lib.ts
ADDED
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
import path from "node:path"
|
|
2
|
+
import { DEFAULT_PROMPT, GUARD } from "./prompt"
|
|
3
|
+
|
|
4
|
+
export const METADATA_KEY = "opencode-summary"
|
|
5
|
+
export type Metadata = Record<string, unknown>
|
|
6
|
+
export type SummaryRecord = { text: string; generatedAt: number; promptId: string; lastMessageID: string }
|
|
7
|
+
|
|
8
|
+
export const buildRequest = (prompt: string) => `${GUARD}\n\n${prompt}`
|
|
9
|
+
|
|
10
|
+
/** FNV-1a 32-bit, hex. Stable id for "generated with a different prompt". */
|
|
11
|
+
export function promptId(text: string): string {
|
|
12
|
+
let hash = 0x811c9dc5
|
|
13
|
+
for (let i = 0; i < text.length; i++) hash = Math.imul(hash ^ text.charCodeAt(i), 0x01000193) >>> 0
|
|
14
|
+
return hash.toString(16).padStart(8, "0")
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function readSummary(metadata: unknown): SummaryRecord | undefined {
|
|
18
|
+
if (!metadata || typeof metadata !== "object") return undefined
|
|
19
|
+
const value = (metadata as Metadata)[METADATA_KEY]
|
|
20
|
+
if (!value || typeof value !== "object") return undefined
|
|
21
|
+
const { text, generatedAt, promptId, lastMessageID } = value as Record<string, unknown>
|
|
22
|
+
if (typeof text !== "string" || typeof generatedAt !== "number") return undefined
|
|
23
|
+
if (typeof promptId !== "string" || typeof lastMessageID !== "string") return undefined
|
|
24
|
+
return { text, generatedAt, promptId, lastMessageID }
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export const withSummary = (metadata: Metadata | undefined, record: SummaryRecord): Metadata => ({
|
|
28
|
+
...metadata,
|
|
29
|
+
[METADATA_KEY]: record,
|
|
30
|
+
})
|
|
31
|
+
|
|
32
|
+
export function withoutSummary(metadata: Metadata | undefined): Metadata {
|
|
33
|
+
const { [METADATA_KEY]: _removed, ...rest } = metadata ?? {}
|
|
34
|
+
return rest
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export const isBlank = (text: string | undefined) => !text || text.trim() === ""
|
|
38
|
+
|
|
39
|
+
/** Copy of tui/src/component/dialog-stash.tsx getRelativeTime; undefined at 7+ days. */
|
|
40
|
+
export function relativeTime(ts: number, now: number): string | undefined {
|
|
41
|
+
const s = Math.floor((now - ts) / 1000)
|
|
42
|
+
const m = Math.floor(s / 60)
|
|
43
|
+
const h = Math.floor(m / 60)
|
|
44
|
+
const d = Math.floor(h / 24)
|
|
45
|
+
if (s < 60) return "just now"
|
|
46
|
+
if (m < 60) return `${m}m ago`
|
|
47
|
+
if (h < 24) return `${h}h ago`
|
|
48
|
+
if (d < 7) return `${d}d ago`
|
|
49
|
+
return undefined
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** Copy of tui/src/util/locale.ts datetime: "<time> · <date>". */
|
|
53
|
+
export function datetime(ts: number): string {
|
|
54
|
+
const date = new Date(ts)
|
|
55
|
+
return `${date.toLocaleTimeString(undefined, { timeStyle: "short" })} · ${date.toLocaleDateString()}`
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export const TURN_LIMIT = 200
|
|
59
|
+
export const GENERATE_TIMEOUT_MS = 180_000
|
|
60
|
+
export type Turns = { count: number; capped: boolean }
|
|
61
|
+
|
|
62
|
+
/** `userMessageIDs` = one page of user messages (newest first, at most `limit`). IDs sort as strings. */
|
|
63
|
+
export function turnsSince(userMessageIDs: readonly string[], lastMessageID: string, limit = TURN_LIMIT): Turns {
|
|
64
|
+
const count = userMessageIDs.filter((id) => id > lastMessageID).length
|
|
65
|
+
return { count, capped: userMessageIDs.length >= limit && count >= limit }
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export function turnsText({ count, capped }: Turns): string {
|
|
69
|
+
if (capped) return `${TURN_LIMIT}+ turns ago`
|
|
70
|
+
if (count === 0) return "up to date"
|
|
71
|
+
return count === 1 ? "1 turn ago" : `${count} turns ago`
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export const turnLabel = (turns: Turns) => `summary ${turnsText(turns)}`
|
|
75
|
+
|
|
76
|
+
export function generatedLine(record: SummaryRecord, now: number, turns?: Turns): string {
|
|
77
|
+
const relative = relativeTime(record.generatedAt, now)
|
|
78
|
+
const parts = ["Generated", ...(relative ? [`${relative} ·`] : []), datetime(record.generatedAt)]
|
|
79
|
+
return turns ? `${parts.join(" ")} · ${turnsText(turns)}` : parts.join(" ")
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export function errorMessage(cause: unknown): string {
|
|
83
|
+
if (cause instanceof Error && cause.message) return cause.message
|
|
84
|
+
return typeof cause === "string" && cause ? cause : "Unknown error"
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export type BlockReason = "running" | "in-flight"
|
|
88
|
+
|
|
89
|
+
export function blockReason(input: { status: "idle" | "running"; inFlight: boolean }): BlockReason | undefined {
|
|
90
|
+
if (input.inFlight) return "in-flight"
|
|
91
|
+
if (input.status === "running") return "running"
|
|
92
|
+
return undefined
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/** Newest visible message id. `idsDesc` is newest first; a staged revert hides ids >= `revertBoundary`. */
|
|
96
|
+
export function lastVisibleID(idsDesc: readonly string[], revertBoundary?: string): string | undefined {
|
|
97
|
+
return revertBoundary === undefined ? idsDesc[0] : idsDesc.find((id) => id < revertBoundary)
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
export function blockMessage(reason: BlockReason): string {
|
|
101
|
+
switch (reason) {
|
|
102
|
+
case "in-flight":
|
|
103
|
+
return "Already generating"
|
|
104
|
+
case "running":
|
|
105
|
+
return "Agent is running - wait for it to finish"
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
export function isMessageNotFound(error: unknown): boolean {
|
|
110
|
+
if (!error || typeof error !== "object") return false
|
|
111
|
+
const { _tag, name } = error as { _tag?: unknown; name?: unknown }
|
|
112
|
+
return _tag === "MessageNotFoundError" || name === "MessageNotFoundError"
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
export type ValidityCheck = { found: true } | { found: false; notFound: boolean }
|
|
116
|
+
export const validityAction = (check: ValidityCheck): "keep" | "delete" =>
|
|
117
|
+
!check.found && check.notFound ? "delete" : "keep"
|
|
118
|
+
|
|
119
|
+
export type PromptSource = "default" | "override"
|
|
120
|
+
export type ResolvedPrompt = { text: string; source: PromptSource }
|
|
121
|
+
|
|
122
|
+
export const resolvePrompt = (override: string | undefined): ResolvedPrompt =>
|
|
123
|
+
isBlank(override) ? { text: DEFAULT_PROMPT, source: "default" } : { text: override!.trim(), source: "override" }
|
|
124
|
+
|
|
125
|
+
export function saveAction(edited: string): "noop" | "delete" | "write" {
|
|
126
|
+
if (isBlank(edited)) return "noop"
|
|
127
|
+
return edited.trim() === DEFAULT_PROMPT.trim() ? "delete" : "write"
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/** User-level prompt override location: `<config home>/opencode/opencode-summary/summary.md`. */
|
|
131
|
+
export function promptPaths(env: { XDG_CONFIG_HOME?: string }, home: string): { dir: string; override: string } {
|
|
132
|
+
const base = env.XDG_CONFIG_HOME?.trim() || path.join(home, ".config")
|
|
133
|
+
const dir = path.join(base, "opencode", "opencode-summary")
|
|
134
|
+
return { dir, override: path.join(dir, "summary.md") }
|
|
135
|
+
}
|
package/src/prompt.ts
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export const GUARD = "Do not call any tools and do not take any actions."
|
|
2
|
+
|
|
3
|
+
export const DEFAULT_PROMPT = `Summarize the conversation so far for the user.
|
|
4
|
+
Output markdown only, no preamble, about 300 words or less. Use exactly these sections, bullets only below TL;DR; omit a section if empty:
|
|
5
|
+
|
|
6
|
+
## TL;DR
|
|
7
|
+
2-3 sentences.
|
|
8
|
+
## Goal
|
|
9
|
+
## Decisions
|
|
10
|
+
## Progress / current state
|
|
11
|
+
## Open questions / next steps
|
|
12
|
+
## Key files and references`
|