@royalcat/opencode-dcp-rc 0.0.0-stage → 4.0.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 +619 -0
- package/README.md +348 -2
- package/dist/index.d.ts +7 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +8129 -0
- package/dist/index.js.map +1 -0
- package/dist/lib/commands/compression-targets.d.ts +13 -0
- package/dist/lib/commands/compression-targets.d.ts.map +1 -0
- package/dist/lib/commands/context.d.ts +67 -0
- package/dist/lib/commands/context.d.ts.map +1 -0
- package/dist/lib/commands/decompress.d.ts +12 -0
- package/dist/lib/commands/decompress.d.ts.map +1 -0
- package/dist/lib/commands/help.d.ts +18 -0
- package/dist/lib/commands/help.d.ts.map +1 -0
- package/dist/lib/commands/index.d.ts +8 -0
- package/dist/lib/commands/index.d.ts.map +1 -0
- package/dist/lib/commands/manual.d.ts +23 -0
- package/dist/lib/commands/manual.d.ts.map +1 -0
- package/dist/lib/commands/recompress.d.ts +12 -0
- package/dist/lib/commands/recompress.d.ts.map +1 -0
- package/dist/lib/commands/stats.d.ts +27 -0
- package/dist/lib/commands/stats.d.ts.map +1 -0
- package/dist/lib/commands/sweep.d.ts +23 -0
- package/dist/lib/commands/sweep.d.ts.map +1 -0
- package/dist/lib/compress/index.d.ts +3 -0
- package/dist/lib/compress/index.d.ts.map +1 -0
- package/dist/lib/compress/pipeline.d.ts +29 -0
- package/dist/lib/compress/pipeline.d.ts.map +1 -0
- package/dist/lib/compress/protected-content.d.ts +6 -0
- package/dist/lib/compress/protected-content.d.ts.map +1 -0
- package/dist/lib/compress/rc.d.ts +15 -0
- package/dist/lib/compress/rc.d.ts.map +1 -0
- package/dist/lib/compress/search.d.ts +12 -0
- package/dist/lib/compress/search.d.ts.map +1 -0
- package/dist/lib/compress/state.d.ts +10 -0
- package/dist/lib/compress/state.d.ts.map +1 -0
- package/dist/lib/compress/summary.d.ts +68 -0
- package/dist/lib/compress/summary.d.ts.map +1 -0
- package/dist/lib/compress/timing.d.ts +18 -0
- package/dist/lib/compress/timing.d.ts.map +1 -0
- package/dist/lib/compress/types.d.ts +64 -0
- package/dist/lib/compress/types.d.ts.map +1 -0
- package/dist/lib/compress/usage.d.ts +45 -0
- package/dist/lib/compress/usage.d.ts.map +1 -0
- package/dist/lib/compress-permission.d.ts +4 -0
- package/dist/lib/compress-permission.d.ts.map +1 -0
- package/dist/lib/config.d.ts +83 -0
- package/dist/lib/config.d.ts.map +1 -0
- package/dist/lib/hooks.d.ts +22 -0
- package/dist/lib/hooks.d.ts.map +1 -0
- package/dist/lib/logger.d.ts +31 -0
- package/dist/lib/logger.d.ts.map +1 -0
- package/dist/lib/message-ids.d.ts +20 -0
- package/dist/lib/message-ids.d.ts.map +1 -0
- package/dist/lib/messages/index.d.ts +8 -0
- package/dist/lib/messages/index.d.ts.map +1 -0
- package/dist/lib/messages/inject/inject.d.ts +7 -0
- package/dist/lib/messages/inject/inject.d.ts.map +1 -0
- package/dist/lib/messages/inject/subagent-results.d.ts +4 -0
- package/dist/lib/messages/inject/subagent-results.d.ts.map +1 -0
- package/dist/lib/messages/inject/utils.d.ts +23 -0
- package/dist/lib/messages/inject/utils.d.ts.map +1 -0
- package/dist/lib/messages/prune.d.ts +4 -0
- package/dist/lib/messages/prune.d.ts.map +1 -0
- package/dist/lib/messages/query.d.ts +7 -0
- package/dist/lib/messages/query.d.ts.map +1 -0
- package/dist/lib/messages/reasoning-strip.d.ts +8 -0
- package/dist/lib/messages/reasoning-strip.d.ts.map +1 -0
- package/dist/lib/messages/shape.d.ts +5 -0
- package/dist/lib/messages/shape.d.ts.map +1 -0
- package/dist/lib/messages/sync.d.ts +4 -0
- package/dist/lib/messages/sync.d.ts.map +1 -0
- package/dist/lib/messages/utils.d.ts +27 -0
- package/dist/lib/messages/utils.d.ts.map +1 -0
- package/dist/lib/prompts/compress-rc.d.ts +4 -0
- package/dist/lib/prompts/compress-rc.d.ts.map +1 -0
- package/dist/lib/prompts/context-limit-nudge.d.ts +2 -0
- package/dist/lib/prompts/context-limit-nudge.d.ts.map +1 -0
- package/dist/lib/prompts/extensions/nudge.d.ts +4 -0
- package/dist/lib/prompts/extensions/nudge.d.ts.map +1 -0
- package/dist/lib/prompts/extensions/system.d.ts +4 -0
- package/dist/lib/prompts/extensions/system.d.ts.map +1 -0
- package/dist/lib/prompts/extensions/tool.d.ts +3 -0
- package/dist/lib/prompts/extensions/tool.d.ts.map +1 -0
- package/dist/lib/prompts/index.d.ts +4 -0
- package/dist/lib/prompts/index.d.ts.map +1 -0
- package/dist/lib/prompts/iteration-nudge.d.ts +2 -0
- package/dist/lib/prompts/iteration-nudge.d.ts.map +1 -0
- package/dist/lib/prompts/store.d.ts +26 -0
- package/dist/lib/prompts/store.d.ts.map +1 -0
- package/dist/lib/prompts/system.d.ts +4 -0
- package/dist/lib/prompts/system.d.ts.map +1 -0
- package/dist/lib/prompts/turn-nudge.d.ts +2 -0
- package/dist/lib/prompts/turn-nudge.d.ts.map +1 -0
- package/dist/lib/protected-patterns.d.ts +6 -0
- package/dist/lib/protected-patterns.d.ts.map +1 -0
- package/dist/lib/state/index.d.ts +5 -0
- package/dist/lib/state/index.d.ts.map +1 -0
- package/dist/lib/state/persistence.d.ts +47 -0
- package/dist/lib/state/persistence.d.ts.map +1 -0
- package/dist/lib/state/state.d.ts +9 -0
- package/dist/lib/state/state.d.ts.map +1 -0
- package/dist/lib/state/tool-cache.d.ts +13 -0
- package/dist/lib/state/tool-cache.d.ts.map +1 -0
- package/dist/lib/state/types.d.ts +105 -0
- package/dist/lib/state/types.d.ts.map +1 -0
- package/dist/lib/state/utils.d.ts +22 -0
- package/dist/lib/state/utils.d.ts.map +1 -0
- package/dist/lib/strategies/deduplication.d.ts +10 -0
- package/dist/lib/strategies/deduplication.d.ts.map +1 -0
- package/dist/lib/strategies/index.d.ts +3 -0
- package/dist/lib/strategies/index.d.ts.map +1 -0
- package/dist/lib/strategies/purge-errors.d.ts +13 -0
- package/dist/lib/strategies/purge-errors.d.ts.map +1 -0
- package/dist/lib/subagents/subagent-results.d.ts +5 -0
- package/dist/lib/subagents/subagent-results.d.ts.map +1 -0
- package/dist/lib/token-utils.d.ts +19 -0
- package/dist/lib/token-utils.d.ts.map +1 -0
- package/dist/lib/tui/dialogs.d.ts +29 -0
- package/dist/lib/tui/dialogs.d.ts.map +1 -0
- package/dist/lib/tui/format.d.ts +4 -0
- package/dist/lib/tui/format.d.ts.map +1 -0
- package/dist/lib/tui/types.d.ts +18 -0
- package/dist/lib/tui/types.d.ts.map +1 -0
- package/dist/lib/tui/ui.d.ts +48 -0
- package/dist/lib/tui/ui.d.ts.map +1 -0
- package/dist/lib/ui/notification.d.ts +17 -0
- package/dist/lib/ui/notification.d.ts.map +1 -0
- package/dist/lib/ui/utils.d.ts +10 -0
- package/dist/lib/ui/utils.d.ts.map +1 -0
- package/dist/lib/v2/index.d.ts +5 -0
- package/dist/lib/v2/index.d.ts.map +1 -0
- package/dist/lib/v2/messages.d.ts +13 -0
- package/dist/lib/v2/messages.d.ts.map +1 -0
- package/dist/lib/v2/rpc.d.ts +75 -0
- package/dist/lib/v2/rpc.d.ts.map +1 -0
- package/dist/lib/v2/theme.d.ts +55 -0
- package/dist/lib/v2/theme.d.ts.map +1 -0
- package/dist/lib/v2/tui.d.ts +4 -0
- package/dist/lib/v2/tui.d.ts.map +1 -0
- package/dist/lib/v2/usage.d.ts +44 -0
- package/dist/lib/v2/usage.d.ts.map +1 -0
- package/dist/tui.d.ts +7 -0
- package/dist/tui.d.ts.map +1 -0
- package/lib/commands/compression-targets.ts +54 -0
- package/lib/commands/context.ts +305 -0
- package/lib/commands/decompress.ts +273 -0
- package/lib/commands/help.ts +76 -0
- package/lib/commands/index.ts +11 -0
- package/lib/commands/manual.ts +126 -0
- package/lib/commands/recompress.ts +222 -0
- package/lib/commands/stats.ts +180 -0
- package/lib/commands/sweep.ts +266 -0
- package/lib/compress/index.ts +2 -0
- package/lib/compress/pipeline.ts +116 -0
- package/lib/compress/protected-content.ts +163 -0
- package/lib/compress/rc.ts +494 -0
- package/lib/compress/search.ts +277 -0
- package/lib/compress/state.ts +272 -0
- package/lib/compress/summary.ts +320 -0
- package/lib/compress/timing.ts +77 -0
- package/lib/compress/types.ts +70 -0
- package/lib/compress/usage.ts +168 -0
- package/lib/compress-permission.ts +9 -0
- package/lib/config.ts +995 -0
- package/lib/hooks.ts +205 -0
- package/lib/logger.ts +226 -0
- package/lib/message-ids.ts +181 -0
- package/lib/messages/index.ts +7 -0
- package/lib/messages/inject/inject.ts +215 -0
- package/lib/messages/inject/subagent-results.ts +84 -0
- package/lib/messages/inject/utils.ts +304 -0
- package/lib/messages/prune.ts +238 -0
- package/lib/messages/query.ts +71 -0
- package/lib/messages/reasoning-strip.ts +40 -0
- package/lib/messages/shape.ts +50 -0
- package/lib/messages/sync.ts +124 -0
- package/lib/messages/utils.ts +190 -0
- package/lib/prompts/compress-rc.ts +25 -0
- package/lib/prompts/context-limit-nudge.ts +18 -0
- package/lib/prompts/extensions/nudge.ts +34 -0
- package/lib/prompts/extensions/system.ts +32 -0
- package/lib/prompts/extensions/tool.ts +21 -0
- package/lib/prompts/index.ts +29 -0
- package/lib/prompts/iteration-nudge.ts +6 -0
- package/lib/prompts/store.ts +452 -0
- package/lib/prompts/system.ts +41 -0
- package/lib/prompts/turn-nudge.ts +10 -0
- package/lib/protected-patterns.ts +159 -0
- package/lib/state/index.ts +4 -0
- package/lib/state/persistence.ts +321 -0
- package/lib/state/state.ts +214 -0
- package/lib/state/tool-cache.ts +99 -0
- package/lib/state/types.ts +117 -0
- package/lib/state/utils.ts +345 -0
- package/lib/strategies/deduplication.ts +125 -0
- package/lib/strategies/index.ts +2 -0
- package/lib/strategies/purge-errors.ts +86 -0
- package/lib/subagents/subagent-results.ts +75 -0
- package/lib/token-utils.ts +164 -0
- package/lib/tui/dialogs.tsx +261 -0
- package/lib/tui/format.ts +25 -0
- package/lib/tui/types.ts +25 -0
- package/lib/tui/ui.tsx +245 -0
- package/lib/ui/notification.ts +347 -0
- package/lib/ui/utils.ts +304 -0
- package/lib/v2/index.ts +448 -0
- package/lib/v2/messages.ts +252 -0
- package/lib/v2/rpc.ts +60 -0
- package/lib/v2/theme.ts +67 -0
- package/lib/v2/tui.tsx +115 -0
- package/lib/v2/usage.ts +423 -0
- package/package.json +91 -5
- package/server.js +1 -0
- package/tui.tsx +7 -0
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import type { WithParts } from "../state"
|
|
2
|
+
|
|
3
|
+
const SUB_AGENT_RESULT_BLOCK_REGEX =
|
|
4
|
+
/(<(?:task_result|subagent\b[^>]*)>\s*)([\s\S]*?)(\s*<\/(?:task_result|subagent)>)/i
|
|
5
|
+
|
|
6
|
+
export function getSubAgentId(part: any): string | null {
|
|
7
|
+
const sessionId = part?.state?.metadata?.sessionId ?? part?.state?.metadata?.sessionID
|
|
8
|
+
if (typeof sessionId !== "string") {
|
|
9
|
+
return null
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
const value = sessionId.trim()
|
|
13
|
+
return value.length > 0 ? value : null
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function buildSubagentResultText(messages: WithParts[]): string {
|
|
17
|
+
const assistantMessages = messages.filter((message) => message.info.role === "assistant")
|
|
18
|
+
if (assistantMessages.length === 0) {
|
|
19
|
+
return ""
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const lastAssistant = assistantMessages[assistantMessages.length - 1]
|
|
23
|
+
const lastText = getLastTextPart(lastAssistant)
|
|
24
|
+
|
|
25
|
+
if (assistantMessages.length < 2) {
|
|
26
|
+
return lastText
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const secondToLastAssistant = assistantMessages[assistantMessages.length - 2]
|
|
30
|
+
if (!assistantMessageHasCompressTool(secondToLastAssistant)) {
|
|
31
|
+
return lastText
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const secondToLastText = getLastTextPart(secondToLastAssistant)
|
|
35
|
+
return [secondToLastText, lastText].filter((text) => text.length > 0).join("\n\n")
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function mergeSubagentResult(output: string, subAgentResultText: string): string {
|
|
39
|
+
if (!subAgentResultText || typeof output !== "string") {
|
|
40
|
+
return output
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
return output.replace(
|
|
44
|
+
SUB_AGENT_RESULT_BLOCK_REGEX,
|
|
45
|
+
(_match, openTag: string, _body: string, closeTag: string) =>
|
|
46
|
+
`${openTag}${subAgentResultText}${closeTag}`,
|
|
47
|
+
)
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function getLastTextPart(message: WithParts): string {
|
|
51
|
+
const parts = Array.isArray(message.parts) ? message.parts : []
|
|
52
|
+
for (let index = parts.length - 1; index >= 0; index--) {
|
|
53
|
+
const part = parts[index]
|
|
54
|
+
if (part.type !== "text" || typeof part.text !== "string") {
|
|
55
|
+
continue
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const text = part.text.trim()
|
|
59
|
+
if (!text) {
|
|
60
|
+
continue
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
return text
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
return ""
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function assistantMessageHasCompressTool(message: WithParts): boolean {
|
|
70
|
+
const parts = Array.isArray(message.parts) ? message.parts : []
|
|
71
|
+
return parts.some(
|
|
72
|
+
(part) =>
|
|
73
|
+
part.type === "tool" && part.tool === "compress" && part.state?.status === "completed",
|
|
74
|
+
)
|
|
75
|
+
}
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
import { SessionState, WithParts } from "./state"
|
|
2
|
+
import { AssistantMessage, UserMessage } from "@opencode-ai/sdk/v2"
|
|
3
|
+
import { Logger } from "./logger"
|
|
4
|
+
import * as _anthropicTokenizer from "@anthropic-ai/tokenizer"
|
|
5
|
+
const anthropicCountTokens = (_anthropicTokenizer.countTokens ??
|
|
6
|
+
(_anthropicTokenizer as any).default?.countTokens) as typeof _anthropicTokenizer.countTokens
|
|
7
|
+
import { getLastUserMessage } from "./messages/query"
|
|
8
|
+
|
|
9
|
+
export function getCurrentTokenUsage(state: SessionState, messages: WithParts[]): number {
|
|
10
|
+
for (let i = messages.length - 1; i >= 0; i--) {
|
|
11
|
+
const msg = messages[i]
|
|
12
|
+
if (msg.info.role !== "assistant") {
|
|
13
|
+
continue
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const assistantInfo = msg.info as AssistantMessage
|
|
17
|
+
if ((assistantInfo.tokens?.output || 0) <= 0) {
|
|
18
|
+
continue
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
if (
|
|
22
|
+
state.lastCompaction > 0 &&
|
|
23
|
+
(msg.info.time.created < state.lastCompaction ||
|
|
24
|
+
(msg.info.summary === true && msg.info.time.created === state.lastCompaction))
|
|
25
|
+
) {
|
|
26
|
+
return 0
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const input = assistantInfo.tokens?.input || 0
|
|
30
|
+
const output = assistantInfo.tokens?.output || 0
|
|
31
|
+
const reasoning = assistantInfo.tokens?.reasoning || 0
|
|
32
|
+
const cacheRead = assistantInfo.tokens?.cache?.read || 0
|
|
33
|
+
const cacheWrite = assistantInfo.tokens?.cache?.write || 0
|
|
34
|
+
return input + output + reasoning + cacheRead + cacheWrite
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
return 0
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function getCurrentParams(
|
|
41
|
+
state: SessionState,
|
|
42
|
+
messages: WithParts[],
|
|
43
|
+
logger: Logger,
|
|
44
|
+
): {
|
|
45
|
+
providerId: string | undefined
|
|
46
|
+
modelId: string | undefined
|
|
47
|
+
agent: string | undefined
|
|
48
|
+
variant: string | undefined
|
|
49
|
+
} {
|
|
50
|
+
const userMsg = getLastUserMessage(messages)
|
|
51
|
+
if (!userMsg) {
|
|
52
|
+
logger.debug("No user message found when determining current params")
|
|
53
|
+
return {
|
|
54
|
+
providerId: undefined,
|
|
55
|
+
modelId: undefined,
|
|
56
|
+
agent: undefined,
|
|
57
|
+
variant: undefined,
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
const userInfo = userMsg.info as UserMessage
|
|
61
|
+
const agent: string = userInfo.agent
|
|
62
|
+
const providerId: string | undefined = userInfo.model.providerID
|
|
63
|
+
const modelId: string | undefined = userInfo.model.modelID
|
|
64
|
+
const variant: string | undefined = userInfo.model.variant
|
|
65
|
+
|
|
66
|
+
return { providerId, modelId, agent, variant }
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export function countTokens(text: string): number {
|
|
70
|
+
if (!text) return 0
|
|
71
|
+
try {
|
|
72
|
+
return anthropicCountTokens(text)
|
|
73
|
+
} catch {
|
|
74
|
+
return Math.round(text.length / 4)
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export function estimateTokensBatch(texts: string[]): number {
|
|
79
|
+
if (texts.length === 0) return 0
|
|
80
|
+
return countTokens(texts.join(" "))
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export const COMPACTED_TOOL_OUTPUT_PLACEHOLDER = "[Old tool result content cleared]"
|
|
84
|
+
|
|
85
|
+
function stringifyToolContent(value: unknown): string {
|
|
86
|
+
return typeof value === "string" ? value : JSON.stringify(value)
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export function extractCompletedToolOutput(part: any): string | undefined {
|
|
90
|
+
if (
|
|
91
|
+
part?.type !== "tool" ||
|
|
92
|
+
part.state?.status !== "completed" ||
|
|
93
|
+
part.state?.output === undefined
|
|
94
|
+
) {
|
|
95
|
+
return undefined
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
if (part.state?.time?.compacted) {
|
|
99
|
+
return COMPACTED_TOOL_OUTPUT_PLACEHOLDER
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
return stringifyToolContent(part.state.output)
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
export function extractToolContent(part: any): string[] {
|
|
106
|
+
const contents: string[] = []
|
|
107
|
+
|
|
108
|
+
if (part?.type !== "tool") {
|
|
109
|
+
return contents
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
if (part.state?.input !== undefined) {
|
|
113
|
+
contents.push(stringifyToolContent(part.state.input))
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
const completedOutput = extractCompletedToolOutput(part)
|
|
117
|
+
if (completedOutput !== undefined) {
|
|
118
|
+
contents.push(completedOutput)
|
|
119
|
+
} else if (part.state?.status === "error" && part.state?.error) {
|
|
120
|
+
contents.push(stringifyToolContent(part.state.error))
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
return contents
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
export function countToolTokens(part: any): number {
|
|
127
|
+
const contents = extractToolContent(part)
|
|
128
|
+
return estimateTokensBatch(contents)
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
export function getTotalToolTokens(state: SessionState, toolIds: string[]): number {
|
|
132
|
+
let total = 0
|
|
133
|
+
for (const id of toolIds) {
|
|
134
|
+
const entry = state.toolParameters.get(id)
|
|
135
|
+
total += entry?.tokenCount ?? 0
|
|
136
|
+
}
|
|
137
|
+
return total
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
export function countMessageTextTokens(msg: WithParts): number {
|
|
141
|
+
const texts: string[] = []
|
|
142
|
+
const parts = Array.isArray(msg.parts) ? msg.parts : []
|
|
143
|
+
for (const part of parts) {
|
|
144
|
+
if (part.type === "text") {
|
|
145
|
+
texts.push(part.text)
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
if (texts.length === 0) return 0
|
|
149
|
+
return estimateTokensBatch(texts)
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
export function countAllMessageTokens(msg: WithParts): number {
|
|
153
|
+
const parts = Array.isArray(msg.parts) ? msg.parts : []
|
|
154
|
+
const texts: string[] = []
|
|
155
|
+
for (const part of parts) {
|
|
156
|
+
if (part.type === "text") {
|
|
157
|
+
texts.push(part.text)
|
|
158
|
+
} else {
|
|
159
|
+
texts.push(...extractToolContent(part))
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
if (texts.length === 0) return 0
|
|
163
|
+
return estimateTokensBatch(texts)
|
|
164
|
+
}
|
|
@@ -0,0 +1,261 @@
|
|
|
1
|
+
/** @jsxImportSource @opentui/solid */
|
|
2
|
+
|
|
3
|
+
import type { analyzeContextTokens } from "../commands/context"
|
|
4
|
+
import { formatTokenCount } from "../ui/utils"
|
|
5
|
+
import { formatDuration, formatRatio } from "./format"
|
|
6
|
+
import { ActionRow, Card, DcpFrame, Metric, Progress, PromptRow, StatusPill } from "./ui"
|
|
7
|
+
import type { StatsReport, ViewApi } from "./types"
|
|
8
|
+
|
|
9
|
+
export function StatusDialog(props: {
|
|
10
|
+
api: ViewApi
|
|
11
|
+
title: string
|
|
12
|
+
eyebrow: string
|
|
13
|
+
message: string
|
|
14
|
+
}) {
|
|
15
|
+
return (
|
|
16
|
+
<DcpFrame api={props.api} title={props.title} eyebrow={props.eyebrow}>
|
|
17
|
+
<box paddingTop={1} paddingBottom={1}>
|
|
18
|
+
<text fg={props.api.theme.current.textMuted}>{props.message}</text>
|
|
19
|
+
</box>
|
|
20
|
+
</DcpFrame>
|
|
21
|
+
)
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function ContextDialog(props: {
|
|
25
|
+
api: ViewApi
|
|
26
|
+
breakdown: ReturnType<typeof analyzeContextTokens>
|
|
27
|
+
onBack: () => void
|
|
28
|
+
}) {
|
|
29
|
+
const theme = props.api.theme.current
|
|
30
|
+
const breakdown = props.breakdown
|
|
31
|
+
const total = Math.max(0, breakdown.total)
|
|
32
|
+
const activePruned = breakdown.prunedToolCount + breakdown.prunedMessageCount
|
|
33
|
+
|
|
34
|
+
return (
|
|
35
|
+
<DcpFrame api={props.api} title="Context" eyebrow="DCP" onBack={props.onBack}>
|
|
36
|
+
<Card theme={theme} title="Current">
|
|
37
|
+
<Metric
|
|
38
|
+
theme={theme}
|
|
39
|
+
label="Total in context"
|
|
40
|
+
value={`~${formatTokenCount(total)}`}
|
|
41
|
+
hint="tokens"
|
|
42
|
+
/>
|
|
43
|
+
<Metric
|
|
44
|
+
theme={theme}
|
|
45
|
+
label="Tools in context"
|
|
46
|
+
value={`${breakdown.toolsInContextCount}`}
|
|
47
|
+
/>
|
|
48
|
+
<Metric theme={theme} label="Active pruned targets" value={`${activePruned}`} />
|
|
49
|
+
<Metric
|
|
50
|
+
theme={theme}
|
|
51
|
+
label="Tokens pruned"
|
|
52
|
+
value={`~${formatTokenCount(breakdown.prunedTokens)}`}
|
|
53
|
+
hint="tokens"
|
|
54
|
+
/>
|
|
55
|
+
</Card>
|
|
56
|
+
<Card theme={theme} title="Breakdown">
|
|
57
|
+
<Progress
|
|
58
|
+
theme={theme}
|
|
59
|
+
label="System"
|
|
60
|
+
value={breakdown.system}
|
|
61
|
+
total={total}
|
|
62
|
+
color="primary"
|
|
63
|
+
detail={`~${formatTokenCount(breakdown.system)} tokens`}
|
|
64
|
+
/>
|
|
65
|
+
<Progress
|
|
66
|
+
theme={theme}
|
|
67
|
+
label="User"
|
|
68
|
+
value={breakdown.user}
|
|
69
|
+
total={total}
|
|
70
|
+
color="primary"
|
|
71
|
+
detail={`~${formatTokenCount(breakdown.user)} tokens`}
|
|
72
|
+
/>
|
|
73
|
+
<Progress
|
|
74
|
+
theme={theme}
|
|
75
|
+
label="Assistant"
|
|
76
|
+
value={breakdown.assistant}
|
|
77
|
+
total={total}
|
|
78
|
+
color="primary"
|
|
79
|
+
detail={`~${formatTokenCount(breakdown.assistant)} tokens`}
|
|
80
|
+
/>
|
|
81
|
+
<Progress
|
|
82
|
+
theme={theme}
|
|
83
|
+
label={`Tools (${breakdown.toolsInContextCount})`}
|
|
84
|
+
value={breakdown.tools}
|
|
85
|
+
total={total}
|
|
86
|
+
color="primary"
|
|
87
|
+
detail={`~${formatTokenCount(breakdown.tools)} tokens`}
|
|
88
|
+
/>
|
|
89
|
+
</Card>
|
|
90
|
+
</DcpFrame>
|
|
91
|
+
)
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export function StatsDialog(props: { api: ViewApi; report: StatsReport; onBack: () => void }) {
|
|
95
|
+
const theme = props.api.theme.current
|
|
96
|
+
const ratio = formatRatio(props.report.sessionTokens, props.report.sessionSummaryTokens)
|
|
97
|
+
return (
|
|
98
|
+
<DcpFrame api={props.api} title="Stats" eyebrow="DCP" onBack={props.onBack}>
|
|
99
|
+
<Card theme={theme} title="Session">
|
|
100
|
+
<Metric
|
|
101
|
+
theme={theme}
|
|
102
|
+
label="Tokens saved"
|
|
103
|
+
value={`~${formatTokenCount(props.report.sessionTokens)}`}
|
|
104
|
+
hint="tokens"
|
|
105
|
+
/>
|
|
106
|
+
<Metric
|
|
107
|
+
theme={theme}
|
|
108
|
+
label="Summary size"
|
|
109
|
+
value={`~${formatTokenCount(props.report.sessionSummaryTokens)}`}
|
|
110
|
+
hint="tokens"
|
|
111
|
+
/>
|
|
112
|
+
<Metric theme={theme} label="Compression ratio" value={ratio} />
|
|
113
|
+
<Metric
|
|
114
|
+
theme={theme}
|
|
115
|
+
label="Compression time"
|
|
116
|
+
value={formatDuration(props.report.sessionDurationMs)}
|
|
117
|
+
/>
|
|
118
|
+
<Metric theme={theme} label="Tools pruned" value={`${props.report.sessionTools}`} />
|
|
119
|
+
<Metric
|
|
120
|
+
theme={theme}
|
|
121
|
+
label="Messages pruned"
|
|
122
|
+
value={`${props.report.sessionMessages}`}
|
|
123
|
+
/>
|
|
124
|
+
<Metric
|
|
125
|
+
theme={theme}
|
|
126
|
+
label="Summary requests"
|
|
127
|
+
value={`${props.report.sessionCompressionUsage.calls}`}
|
|
128
|
+
hint={`${props.report.sessionCompressionUsage.providerCalls} provider · ${props.report.sessionCompressionUsage.estimatedCalls} estimated`}
|
|
129
|
+
/>
|
|
130
|
+
<Metric
|
|
131
|
+
theme={theme}
|
|
132
|
+
label="Request in|out"
|
|
133
|
+
value={`~${formatTokenCount(props.report.sessionCompressionUsage.inputTokens)} | ~${formatTokenCount(props.report.sessionCompressionUsage.outputTokens)}`}
|
|
134
|
+
hint="tokens"
|
|
135
|
+
/>
|
|
136
|
+
<Metric
|
|
137
|
+
theme={theme}
|
|
138
|
+
label="Request cache r|w"
|
|
139
|
+
value={`~${formatTokenCount(props.report.sessionCompressionUsage.cacheReadTokens)} | ~${formatTokenCount(props.report.sessionCompressionUsage.cacheWriteTokens)}`}
|
|
140
|
+
hint={`reasoning ~${formatTokenCount(props.report.sessionCompressionUsage.reasoningTokens)}`}
|
|
141
|
+
/>
|
|
142
|
+
</Card>
|
|
143
|
+
<Card theme={theme} title="All time">
|
|
144
|
+
<Metric
|
|
145
|
+
theme={theme}
|
|
146
|
+
label="Tokens saved"
|
|
147
|
+
value={`~${formatTokenCount(props.report.allTime.totalTokens)}`}
|
|
148
|
+
hint="tokens"
|
|
149
|
+
/>
|
|
150
|
+
<Metric
|
|
151
|
+
theme={theme}
|
|
152
|
+
label="Tools pruned"
|
|
153
|
+
value={`${props.report.allTime.totalTools}`}
|
|
154
|
+
/>
|
|
155
|
+
<Metric
|
|
156
|
+
theme={theme}
|
|
157
|
+
label="Messages pruned"
|
|
158
|
+
value={`${props.report.allTime.totalMessages}`}
|
|
159
|
+
/>
|
|
160
|
+
<Metric
|
|
161
|
+
theme={theme}
|
|
162
|
+
label="Sessions with DCP history"
|
|
163
|
+
value={`${props.report.allTime.sessionCount}`}
|
|
164
|
+
/>
|
|
165
|
+
<Metric
|
|
166
|
+
theme={theme}
|
|
167
|
+
label="Summary requests"
|
|
168
|
+
value={`${props.report.allTime.compressionUsage.calls}`}
|
|
169
|
+
hint={`~${formatTokenCount(props.report.allTime.compressionUsage.inputTokens)} in · ~${formatTokenCount(props.report.allTime.compressionUsage.outputTokens)} out`}
|
|
170
|
+
/>
|
|
171
|
+
</Card>
|
|
172
|
+
</DcpFrame>
|
|
173
|
+
)
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
export function PanelDialog(props: {
|
|
177
|
+
api: ViewApi
|
|
178
|
+
manualMode: boolean
|
|
179
|
+
canCompress: boolean
|
|
180
|
+
blockedReason?: string
|
|
181
|
+
onContext: () => void
|
|
182
|
+
onStats: () => void
|
|
183
|
+
onManual: (enabled: boolean) => void
|
|
184
|
+
}) {
|
|
185
|
+
const theme = props.api.theme.current
|
|
186
|
+
const canCompress = props.canCompress
|
|
187
|
+
return (
|
|
188
|
+
<DcpFrame api={props.api} eyebrow="DCP">
|
|
189
|
+
<Card theme={theme} title="Views">
|
|
190
|
+
<box flexDirection="column" gap={1}>
|
|
191
|
+
<ActionRow
|
|
192
|
+
theme={theme}
|
|
193
|
+
title="Context"
|
|
194
|
+
detail="Token usage"
|
|
195
|
+
onClick={props.onContext}
|
|
196
|
+
/>
|
|
197
|
+
<ActionRow
|
|
198
|
+
theme={theme}
|
|
199
|
+
title="Stats"
|
|
200
|
+
detail="Savings"
|
|
201
|
+
onClick={props.onStats}
|
|
202
|
+
/>
|
|
203
|
+
</box>
|
|
204
|
+
</Card>
|
|
205
|
+
<Card theme={theme} title="Prompt">
|
|
206
|
+
{canCompress ? (
|
|
207
|
+
<PromptRow
|
|
208
|
+
theme={theme}
|
|
209
|
+
command="/dcp-compress [focus]"
|
|
210
|
+
description="Ask the model to compress"
|
|
211
|
+
accent="primary"
|
|
212
|
+
/>
|
|
213
|
+
) : (
|
|
214
|
+
<text fg={theme.textMuted}>
|
|
215
|
+
{props.blockedReason ?? "Compression is denied by permissions."}
|
|
216
|
+
</text>
|
|
217
|
+
)}
|
|
218
|
+
</Card>
|
|
219
|
+
<Card theme={theme} title="Session State">
|
|
220
|
+
<ManualModeToggle
|
|
221
|
+
api={props.api}
|
|
222
|
+
enabled={props.manualMode}
|
|
223
|
+
onToggle={props.onManual}
|
|
224
|
+
/>
|
|
225
|
+
<StatusPill
|
|
226
|
+
theme={theme}
|
|
227
|
+
label="Compression command"
|
|
228
|
+
value={canCompress ? "enabled" : "disabled"}
|
|
229
|
+
accent={canCompress ? "success" : "warning"}
|
|
230
|
+
/>
|
|
231
|
+
</Card>
|
|
232
|
+
</DcpFrame>
|
|
233
|
+
)
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
function ManualModeToggle(props: {
|
|
237
|
+
api: ViewApi
|
|
238
|
+
enabled: boolean
|
|
239
|
+
onToggle: (enabled: boolean) => void
|
|
240
|
+
}) {
|
|
241
|
+
const theme = props.api.theme.current
|
|
242
|
+
const enabled = props.enabled
|
|
243
|
+
const track = enabled ? theme.success : theme.error
|
|
244
|
+
return (
|
|
245
|
+
<box flexDirection="row" justifyContent="space-between" paddingLeft={1} paddingRight={1}>
|
|
246
|
+
<box width={22}>
|
|
247
|
+
<text fg={theme.primary}>
|
|
248
|
+
<b>Manual mode</b>
|
|
249
|
+
</text>
|
|
250
|
+
</box>
|
|
251
|
+
<box
|
|
252
|
+
backgroundColor={track}
|
|
253
|
+
paddingLeft={1}
|
|
254
|
+
paddingRight={1}
|
|
255
|
+
onMouseUp={() => props.onToggle(!enabled)}
|
|
256
|
+
>
|
|
257
|
+
<text fg={theme.background}>{enabled ? " ■" : "■ "}</text>
|
|
258
|
+
</box>
|
|
259
|
+
</box>
|
|
260
|
+
)
|
|
261
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
export function formatDuration(ms: number): string {
|
|
2
|
+
const safeMs = Math.max(0, Math.round(ms))
|
|
3
|
+
if (safeMs < 1000) return `${safeMs} ms`
|
|
4
|
+
|
|
5
|
+
const totalSeconds = safeMs / 1000
|
|
6
|
+
if (totalSeconds < 60) return `${totalSeconds.toFixed(1)} s`
|
|
7
|
+
|
|
8
|
+
const wholeSeconds = Math.floor(totalSeconds)
|
|
9
|
+
const hours = Math.floor(wholeSeconds / 3600)
|
|
10
|
+
const minutes = Math.floor((wholeSeconds % 3600) / 60)
|
|
11
|
+
const seconds = wholeSeconds % 60
|
|
12
|
+
if (hours > 0) return `${hours}h ${minutes}m ${seconds}s`
|
|
13
|
+
return `${minutes}m ${seconds}s`
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function formatRatio(inputTokens: number, outputTokens: number): string {
|
|
17
|
+
if (inputTokens <= 0) return "0:1"
|
|
18
|
+
if (outputTokens <= 0) return "∞:1"
|
|
19
|
+
return `${Math.max(1, Math.round(inputTokens / outputTokens))}:1`
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function pct(value: number, total: number): string {
|
|
23
|
+
if (total <= 0) return "0.0%"
|
|
24
|
+
return `${((value / total) * 100).toFixed(1)}%`
|
|
25
|
+
}
|
package/lib/tui/types.ts
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import type { TuiPluginModule } from "@opencode-ai/plugin/tui"
|
|
2
|
+
import type { buildStatsReport } from "../commands/stats"
|
|
3
|
+
|
|
4
|
+
export type TuiApi = Parameters<NonNullable<TuiPluginModule["tui"]>>[0]
|
|
5
|
+
export type Theme = Pick<
|
|
6
|
+
TuiApi["theme"]["current"],
|
|
7
|
+
| "primary"
|
|
8
|
+
| "accent"
|
|
9
|
+
| "text"
|
|
10
|
+
| "textMuted"
|
|
11
|
+
| "background"
|
|
12
|
+
| "backgroundElement"
|
|
13
|
+
| "borderSubtle"
|
|
14
|
+
| "selectedListItemText"
|
|
15
|
+
| "success"
|
|
16
|
+
| "warning"
|
|
17
|
+
| "error"
|
|
18
|
+
>
|
|
19
|
+
export type ThemeColor = keyof Theme
|
|
20
|
+
export type ViewApi = {
|
|
21
|
+
theme: { readonly current: Theme }
|
|
22
|
+
renderer: Pick<TuiApi["renderer"], "height" | "on" | "off">
|
|
23
|
+
ui: { dialog: { clear(): void } }
|
|
24
|
+
}
|
|
25
|
+
export type StatsReport = Awaited<ReturnType<typeof buildStatsReport>>
|