opencode-context-tree 0.1.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.
@@ -0,0 +1,124 @@
1
+ /**
2
+ * Minimal structural transcript types the TUI/server fill from OpenCode's own
3
+ * `Message`/`Part` shapes (DESIGN.md §7), plus pure preview helpers used by
4
+ * `core/tree.ts`.
5
+ *
6
+ * Pure, no OpenCode/opentui/solid-js imports — see test/core-purity.test.ts.
7
+ */
8
+
9
+ export type StepPart = {
10
+ id: string
11
+ type: string
12
+ text?: string
13
+ tool?: string
14
+ callID?: string
15
+ state?: {
16
+ status?: string
17
+ input?: unknown
18
+ output?: string
19
+ title?: string
20
+ time?: { start?: number; end?: number }
21
+ }
22
+ time?: { start?: number; end?: number }
23
+ metadata?: Record<string, unknown>
24
+ }
25
+
26
+ export type TranscriptMessage = {
27
+ id: string
28
+ role: "user" | "assistant"
29
+ time: { created: number; completed?: number }
30
+ tokens?: { input: number; output: number; reasoning: number; cache: { read: number; write: number } }
31
+ /** OpenCode-native compaction summary marker (not the ctree "jump summary", which is a
32
+ * regular user message tagged via `metadata.ctree.kind === "summary"` instead). */
33
+ summary?: boolean
34
+ parts: StepPart[]
35
+ }
36
+
37
+ export type Transcript = {
38
+ sessionID: string
39
+ title: string
40
+ status: "available" | "deleted"
41
+ messages: TranscriptMessage[]
42
+ }
43
+
44
+ /** Part "kind" for filtering/glyph purposes. Anything not text/tool/reasoning (e.g.
45
+ * step-start, step-finish, snapshot, patch, retry) is "other". */
46
+ export function stepKind(part: StepPart): "text" | "tool" | "reasoning" | "other" {
47
+ if (part.type === "text") return "text"
48
+ if (part.type === "tool") return "tool"
49
+ if (part.type === "reasoning") return "reasoning"
50
+ return "other"
51
+ }
52
+
53
+ function flatten(text: string): string {
54
+ return text.replace(/\s+/g, " ").trim()
55
+ }
56
+
57
+ function truncate(text: string, max: number): string {
58
+ if (text.length <= max) return text
59
+ if (max <= 1) return text.slice(0, max)
60
+ return `${text.slice(0, max - 1)}…`
61
+ }
62
+
63
+ /** Best-effort "primary argument" of a tool call, mirroring core/crop.ts's `shortArg`. */
64
+ function primaryArgOf(input: unknown): string {
65
+ if (!input || typeof input !== "object") return ""
66
+ const record = input as Record<string, unknown>
67
+ const candidate = record["command"] ?? record["filePath"] ?? record["path"] ?? record["pattern"] ?? record["url"]
68
+ return typeof candidate === "string" ? candidate : ""
69
+ }
70
+
71
+ /** The shell command of a bash/exec tool call, if any — drives the `[bash $ …]` form. */
72
+ function commandOf(input: unknown): string {
73
+ if (!input || typeof input !== "object") return ""
74
+ const candidate = (input as Record<string, unknown>)["command"]
75
+ return typeof candidate === "string" ? candidate : ""
76
+ }
77
+
78
+ /**
79
+ * One-line, content-forward preview of a single part — the Pi outline × DSH trajectory row
80
+ * (DESIGN.md §7.1). The command/argument is the DSH "payload"; the output snippet is its
81
+ * "result":
82
+ * - tool with a shell command: `[bash $ <cmd>]` (the command is the payload)
83
+ * - other tools: `[<tool>: <arg>] → <output>` (or `[<tool>]` when there is no argument)
84
+ * - text: first 60 chars, newlines flattened
85
+ * - reasoning: literal `(thinking)`
86
+ * - other (step-start/finish/snapshot/patch/retry, …): title or type, first 60 chars
87
+ *
88
+ * The `⚙`/`✗` glyph and error flag are added by the renderer, not here.
89
+ */
90
+ export function partPreview(part: StepPart): string {
91
+ const kind = stepKind(part)
92
+ if (kind === "tool") {
93
+ const tool = part.tool ?? "tool"
94
+ const command = commandOf(part.state?.input)
95
+ if (command) return `[${tool} $ ${truncate(flatten(command), 52)}]`
96
+ const arg = truncate(flatten(primaryArgOf(part.state?.input)), 40)
97
+ const head = arg ? `[${tool}: ${arg}]` : `[${tool}]`
98
+ const output = truncate(flatten(part.state?.output ?? ""), 30)
99
+ return output ? `${head} → ${output}` : head
100
+ }
101
+ if (kind === "reasoning") return "(thinking)"
102
+ if (kind === "text") return truncate(flatten(part.text ?? ""), 60)
103
+ return truncate(flatten(part.state?.title ?? part.type), 60)
104
+ }
105
+
106
+ /**
107
+ * One-line preview of a whole message (DESIGN.md §7.2's `T<n> ● user <preview>`):
108
+ * the first text part if there is one, else a `[tool a, b]` summary of the tool
109
+ * calls it made. Single line, ≤ 60 chars, newlines flattened.
110
+ */
111
+ export function messagePreview(message: TranscriptMessage): string {
112
+ const textPart = message.parts.find((p) => p.type === "text" && p.text)
113
+ if (textPart) return truncate(flatten(textPart.text ?? ""), 60)
114
+
115
+ const tools: string[] = []
116
+ for (const part of message.parts) {
117
+ if (part.type !== "tool") continue
118
+ const name = part.tool ?? "tool"
119
+ if (!tools.includes(name)) tools.push(name)
120
+ }
121
+ if (tools.length > 0) return truncate(`[tool ${tools.join(", ")}]`, 60)
122
+
123
+ return ""
124
+ }