@tarquinen/opencode-dcp 3.1.12 → 3.1.14
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 +14 -9
- package/dist/index.js +105 -39
- package/dist/index.js.map +1 -1
- package/dist/lib/commands/context.d.ts +15 -0
- package/dist/lib/commands/context.d.ts.map +1 -1
- package/dist/lib/commands/help.d.ts +1 -0
- package/dist/lib/commands/help.d.ts.map +1 -1
- package/dist/lib/commands/manual.d.ts +1 -1
- package/dist/lib/commands/manual.d.ts.map +1 -1
- package/dist/lib/commands/stats.d.ts +10 -0
- package/dist/lib/commands/stats.d.ts.map +1 -1
- package/dist/lib/compress/pipeline.d.ts.map +1 -1
- package/dist/lib/hooks.d.ts.map +1 -1
- package/dist/lib/state/persistence.d.ts +3 -0
- package/dist/lib/state/persistence.d.ts.map +1 -1
- package/dist/lib/state/state.d.ts +1 -0
- package/dist/lib/state/state.d.ts.map +1 -1
- package/dist/lib/tui/commands.d.ts +3 -0
- package/dist/lib/tui/commands.d.ts.map +1 -0
- package/dist/lib/tui/data.d.ts +14 -0
- package/dist/lib/tui/data.d.ts.map +1 -0
- package/dist/lib/tui/dialogs.d.ts +30 -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/modals.d.ts +10 -0
- package/dist/lib/tui/modals.d.ts.map +1 -0
- package/dist/lib/tui/types.d.ts +15 -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/tui.d.ts +7 -0
- package/dist/tui.d.ts.map +1 -0
- package/lib/auth.ts +37 -0
- package/lib/commands/compression-targets.ts +137 -0
- package/lib/commands/context.ts +305 -0
- package/lib/commands/decompress.ts +275 -0
- package/lib/commands/help.ts +76 -0
- package/lib/commands/index.ts +11 -0
- package/lib/commands/manual.ts +127 -0
- package/lib/commands/recompress.ts +224 -0
- package/lib/commands/stats.ts +159 -0
- package/lib/commands/sweep.ts +268 -0
- package/lib/compress/index.ts +3 -0
- package/lib/compress/message-utils.ts +250 -0
- package/lib/compress/message.ts +145 -0
- package/lib/compress/pipeline.ts +108 -0
- package/lib/compress/protected-content.ts +208 -0
- package/lib/compress/range-utils.ts +308 -0
- package/lib/compress/range.ts +192 -0
- package/lib/compress/search.ts +267 -0
- package/lib/compress/state.ts +268 -0
- package/lib/compress/timing.ts +77 -0
- package/lib/compress/types.ts +108 -0
- package/lib/compress-permission.ts +25 -0
- package/lib/config.ts +1007 -0
- package/lib/hooks.ts +375 -0
- package/lib/host-permissions.ts +101 -0
- package/lib/logger.ts +226 -0
- package/lib/message-ids.ts +172 -0
- package/lib/messages/index.ts +8 -0
- package/lib/messages/inject/inject.ts +215 -0
- package/lib/messages/inject/subagent-results.ts +82 -0
- package/lib/messages/inject/utils.ts +374 -0
- package/lib/messages/priority.ts +102 -0
- package/lib/messages/prune.ts +233 -0
- package/lib/messages/query.ts +72 -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 +185 -0
- package/lib/prompts/compress-message.ts +43 -0
- package/lib/prompts/compress-range.ts +60 -0
- package/lib/prompts/context-limit-nudge.ts +18 -0
- package/lib/prompts/extensions/nudge.ts +43 -0
- package/lib/prompts/extensions/system.ts +32 -0
- package/lib/prompts/extensions/tool.ts +35 -0
- package/lib/prompts/index.ts +29 -0
- package/lib/prompts/iteration-nudge.ts +6 -0
- package/lib/prompts/store.ts +467 -0
- package/lib/prompts/system.ts +33 -0
- package/lib/prompts/turn-nudge.ts +10 -0
- package/lib/protected-patterns.ts +128 -0
- package/lib/state/index.ts +4 -0
- package/lib/state/persistence.ts +305 -0
- package/lib/state/state.ts +208 -0
- package/lib/state/tool-cache.ts +98 -0
- package/lib/state/types.ts +111 -0
- package/lib/state/utils.ts +345 -0
- package/lib/strategies/deduplication.ts +127 -0
- package/lib/strategies/index.ts +2 -0
- package/lib/strategies/purge-errors.ts +88 -0
- package/lib/subagents/subagent-results.ts +74 -0
- package/lib/token-utils.ts +164 -0
- package/lib/tui/commands.ts +31 -0
- package/lib/tui/data.ts +73 -0
- package/lib/tui/dialogs.tsx +235 -0
- package/lib/tui/format.ts +25 -0
- package/lib/tui/modals.tsx +92 -0
- package/lib/tui/types.ts +16 -0
- package/lib/tui/ui.tsx +219 -0
- package/lib/ui/notification.ts +347 -0
- package/lib/ui/utils.ts +304 -0
- package/lib/update.ts +185 -0
- package/package.json +21 -5
- package/tui.tsx +26 -0
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
/** @jsxImportSource @opentui/solid */
|
|
2
|
+
|
|
3
|
+
import { buildStatsReport } from "../commands/stats"
|
|
4
|
+
import type { PluginConfig } from "../config"
|
|
5
|
+
import { saveManualModeSetting } from "../state/persistence"
|
|
6
|
+
import { loadSessionData, logger } from "./data"
|
|
7
|
+
import { ContextDialog, PanelDialog, StatsDialog, StatusDialog } from "./dialogs"
|
|
8
|
+
import type { TuiApi } from "./types"
|
|
9
|
+
|
|
10
|
+
export function showDialog(api: TuiApi, render: () => any) {
|
|
11
|
+
api.ui.dialog.setSize("xlarge")
|
|
12
|
+
api.ui.dialog.replace(render)
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function showStatusDialog(api: TuiApi, title: string, eyebrow: string, message: string) {
|
|
16
|
+
showDialog(api, () => (
|
|
17
|
+
<StatusDialog api={api} title={title} eyebrow={eyebrow} message={message} />
|
|
18
|
+
))
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function showError(api: TuiApi, title: string, error: unknown) {
|
|
22
|
+
const message = error instanceof Error ? error.message : String(error)
|
|
23
|
+
showStatusDialog(api, title, "DCP Error", message || "Command failed.")
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function openContextModal(api: TuiApi, config: PluginConfig) {
|
|
27
|
+
runModal(api, "Context", async () => {
|
|
28
|
+
const data = await loadSessionData(api, config)
|
|
29
|
+
if (!data) {
|
|
30
|
+
showStatusDialog(api, "Context", "No session", "Open a session first.")
|
|
31
|
+
return
|
|
32
|
+
}
|
|
33
|
+
showDialog(api, () => (
|
|
34
|
+
<ContextDialog
|
|
35
|
+
api={api}
|
|
36
|
+
state={data.state}
|
|
37
|
+
messages={data.messages}
|
|
38
|
+
onBack={() => openPanelModal(api, config)}
|
|
39
|
+
/>
|
|
40
|
+
))
|
|
41
|
+
})
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function openStatsModal(api: TuiApi, config: PluginConfig) {
|
|
45
|
+
runModal(api, "Stats", async () => {
|
|
46
|
+
const data = await loadSessionData(api, config)
|
|
47
|
+
if (!data) {
|
|
48
|
+
showStatusDialog(api, "Stats", "No session", "Open a session first.")
|
|
49
|
+
return
|
|
50
|
+
}
|
|
51
|
+
const report = await buildStatsReport(data.state, logger)
|
|
52
|
+
showDialog(api, () => (
|
|
53
|
+
<StatsDialog api={api} report={report} onBack={() => openPanelModal(api, config)} />
|
|
54
|
+
))
|
|
55
|
+
})
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export function openPanelModal(api: TuiApi, config: PluginConfig) {
|
|
59
|
+
runModal(api, "DCP", async () => {
|
|
60
|
+
const data = await loadSessionData(api, config)
|
|
61
|
+
if (!data) {
|
|
62
|
+
showStatusDialog(api, "DCP", "No session", "Open a session first.")
|
|
63
|
+
return
|
|
64
|
+
}
|
|
65
|
+
showDialog(api, () => (
|
|
66
|
+
<PanelDialog
|
|
67
|
+
api={api}
|
|
68
|
+
state={data.state}
|
|
69
|
+
config={config}
|
|
70
|
+
onContext={() => openContextModal(api, config)}
|
|
71
|
+
onStats={() => openStatsModal(api, config)}
|
|
72
|
+
onManual={(enabled) => setManualMode(api, config, data.state.sessionId, enabled)}
|
|
73
|
+
/>
|
|
74
|
+
))
|
|
75
|
+
})
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function runModal(api: TuiApi, title: string, task: () => Promise<void>) {
|
|
79
|
+
showStatusDialog(api, title, "DCP", "Loading...")
|
|
80
|
+
void task().catch((error) => showError(api, title, error))
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
async function setManualMode(
|
|
84
|
+
api: TuiApi,
|
|
85
|
+
config: PluginConfig,
|
|
86
|
+
sessionID: string | null | undefined,
|
|
87
|
+
enabled: boolean,
|
|
88
|
+
) {
|
|
89
|
+
if (!sessionID) return
|
|
90
|
+
await saveManualModeSetting(sessionID, enabled, logger)
|
|
91
|
+
openPanelModal(api, config)
|
|
92
|
+
}
|
package/lib/tui/types.ts
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
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 = TuiApi["theme"]["current"]
|
|
6
|
+
export type ThemeColor = Exclude<keyof Theme, "thinkingOpacity" | "_hasSelectedListItemText">
|
|
7
|
+
export type StatsReport = Awaited<ReturnType<typeof buildStatsReport>>
|
|
8
|
+
|
|
9
|
+
export type DcpCommand = {
|
|
10
|
+
title: string
|
|
11
|
+
name: string
|
|
12
|
+
description: string
|
|
13
|
+
slashName: string
|
|
14
|
+
slashAliases?: string[]
|
|
15
|
+
run: () => void | Promise<void>
|
|
16
|
+
}
|
package/lib/tui/ui.tsx
ADDED
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
/** @jsxImportSource @opentui/solid */
|
|
2
|
+
|
|
3
|
+
import { TextAttributes } from "@opentui/core"
|
|
4
|
+
import type { JSX } from "solid-js"
|
|
5
|
+
import { pct } from "./format"
|
|
6
|
+
import type { Theme, ThemeColor, TuiApi } from "./types"
|
|
7
|
+
|
|
8
|
+
export function DcpFrame(props: {
|
|
9
|
+
api: TuiApi
|
|
10
|
+
title?: string
|
|
11
|
+
eyebrow: string
|
|
12
|
+
children: JSX.Element
|
|
13
|
+
onBack?: () => void
|
|
14
|
+
}) {
|
|
15
|
+
const theme = props.api.theme.current
|
|
16
|
+
return (
|
|
17
|
+
<box paddingLeft={3} paddingRight={3} paddingBottom={1} gap={1}>
|
|
18
|
+
<box flexDirection="row" justifyContent="space-between">
|
|
19
|
+
<box flexDirection="column">
|
|
20
|
+
<text fg={theme.primary} attributes={TextAttributes.BOLD}>
|
|
21
|
+
{props.eyebrow}
|
|
22
|
+
</text>
|
|
23
|
+
{props.title ? (
|
|
24
|
+
<text fg={theme.text} attributes={TextAttributes.BOLD}>
|
|
25
|
+
{props.title}
|
|
26
|
+
</text>
|
|
27
|
+
) : null}
|
|
28
|
+
</box>
|
|
29
|
+
<text fg={theme.textMuted} onMouseUp={() => props.api.ui.dialog.clear()}>
|
|
30
|
+
esc
|
|
31
|
+
</text>
|
|
32
|
+
</box>
|
|
33
|
+
<box height={1} border={["bottom"]} borderColor={theme.borderSubtle} />
|
|
34
|
+
{props.children}
|
|
35
|
+
<box flexDirection="row" justifyContent="space-between" paddingTop={1}>
|
|
36
|
+
{props.onBack ? (
|
|
37
|
+
<FooterButton
|
|
38
|
+
theme={theme}
|
|
39
|
+
label="back"
|
|
40
|
+
variant="muted"
|
|
41
|
+
onClick={props.onBack}
|
|
42
|
+
/>
|
|
43
|
+
) : (
|
|
44
|
+
<box />
|
|
45
|
+
)}
|
|
46
|
+
<FooterButton
|
|
47
|
+
theme={theme}
|
|
48
|
+
label="close"
|
|
49
|
+
variant="primary"
|
|
50
|
+
onClick={() => props.api.ui.dialog.clear()}
|
|
51
|
+
/>
|
|
52
|
+
</box>
|
|
53
|
+
</box>
|
|
54
|
+
)
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function FooterButton(props: {
|
|
58
|
+
theme: Theme
|
|
59
|
+
label: string
|
|
60
|
+
variant: "muted" | "primary"
|
|
61
|
+
onClick: () => void
|
|
62
|
+
}) {
|
|
63
|
+
const primary = props.variant === "primary"
|
|
64
|
+
return (
|
|
65
|
+
<box
|
|
66
|
+
paddingLeft={2}
|
|
67
|
+
paddingRight={2}
|
|
68
|
+
backgroundColor={primary ? props.theme.primary : props.theme.backgroundElement}
|
|
69
|
+
onMouseUp={props.onClick}
|
|
70
|
+
>
|
|
71
|
+
<text fg={primary ? props.theme.selectedListItemText : props.theme.text}>
|
|
72
|
+
{props.label}
|
|
73
|
+
</text>
|
|
74
|
+
</box>
|
|
75
|
+
)
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export function Card(props: { theme: Theme; title: string; children: JSX.Element }) {
|
|
79
|
+
const accent = props.theme.primary
|
|
80
|
+
return (
|
|
81
|
+
<box
|
|
82
|
+
flexDirection="column"
|
|
83
|
+
paddingLeft={2}
|
|
84
|
+
paddingRight={2}
|
|
85
|
+
paddingTop={1}
|
|
86
|
+
paddingBottom={1}
|
|
87
|
+
backgroundColor={props.theme.backgroundElement}
|
|
88
|
+
border={["left"]}
|
|
89
|
+
borderColor={accent}
|
|
90
|
+
gap={1}
|
|
91
|
+
>
|
|
92
|
+
<text fg={accent} attributes={TextAttributes.BOLD}>
|
|
93
|
+
{props.title}
|
|
94
|
+
</text>
|
|
95
|
+
{props.children}
|
|
96
|
+
</box>
|
|
97
|
+
)
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
export function Metric(props: { theme: Theme; label: string; value: string; hint?: string }) {
|
|
101
|
+
return (
|
|
102
|
+
<box flexDirection="row" gap={2}>
|
|
103
|
+
<box width={24}>
|
|
104
|
+
<text fg={props.theme.textMuted}>{props.label}</text>
|
|
105
|
+
</box>
|
|
106
|
+
<box flexDirection="row" gap={1} flexGrow={1}>
|
|
107
|
+
<text fg={props.theme.text} attributes={TextAttributes.BOLD}>
|
|
108
|
+
{props.value}
|
|
109
|
+
</text>
|
|
110
|
+
{props.hint ? <text fg={props.theme.textMuted}>{props.hint}</text> : null}
|
|
111
|
+
</box>
|
|
112
|
+
</box>
|
|
113
|
+
)
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
export function Progress(props: {
|
|
117
|
+
theme: Theme
|
|
118
|
+
label: string
|
|
119
|
+
value: number
|
|
120
|
+
total: number
|
|
121
|
+
color: ThemeColor
|
|
122
|
+
detail: string
|
|
123
|
+
}) {
|
|
124
|
+
const width = 32
|
|
125
|
+
const filled =
|
|
126
|
+
props.total > 0 ? Math.max(0, Math.round((props.value / props.total) * width)) : 0
|
|
127
|
+
const empty = Math.max(0, width - filled)
|
|
128
|
+
return (
|
|
129
|
+
<box flexDirection="column" gap={0}>
|
|
130
|
+
<box flexDirection="row" gap={2}>
|
|
131
|
+
<box width={20}>
|
|
132
|
+
<text fg={props.theme.text}>{props.label}</text>
|
|
133
|
+
</box>
|
|
134
|
+
<box flexDirection="row" gap={1} flexGrow={1}>
|
|
135
|
+
<text fg={props.theme.text} attributes={TextAttributes.BOLD}>
|
|
136
|
+
{pct(props.value, props.total)}
|
|
137
|
+
</text>
|
|
138
|
+
<text fg={props.theme.textMuted}>{props.detail}</text>
|
|
139
|
+
</box>
|
|
140
|
+
</box>
|
|
141
|
+
<box flexDirection="row">
|
|
142
|
+
<text fg={props.theme[props.color]}>{"█".repeat(filled)}</text>
|
|
143
|
+
<text fg={props.theme.borderSubtle}>{"░".repeat(empty)}</text>
|
|
144
|
+
</box>
|
|
145
|
+
</box>
|
|
146
|
+
)
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
export function PromptRow(props: {
|
|
150
|
+
theme: Theme
|
|
151
|
+
command: string
|
|
152
|
+
description: string
|
|
153
|
+
accent?: ThemeColor
|
|
154
|
+
}) {
|
|
155
|
+
const accent = props.theme[props.accent ?? "accent"]
|
|
156
|
+
return (
|
|
157
|
+
<box flexDirection="row" gap={2}>
|
|
158
|
+
<box width={22}>
|
|
159
|
+
<text fg={accent} attributes={TextAttributes.BOLD}>
|
|
160
|
+
{props.command}
|
|
161
|
+
</text>
|
|
162
|
+
</box>
|
|
163
|
+
<box flexGrow={1}>
|
|
164
|
+
<text fg={props.theme.text}>{props.description}</text>
|
|
165
|
+
</box>
|
|
166
|
+
</box>
|
|
167
|
+
)
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
export function StatusPill(props: {
|
|
171
|
+
theme: Theme
|
|
172
|
+
label: string
|
|
173
|
+
value: string
|
|
174
|
+
accent: ThemeColor
|
|
175
|
+
}) {
|
|
176
|
+
const accent = props.theme[props.accent]
|
|
177
|
+
return (
|
|
178
|
+
<box flexDirection="row" justifyContent="space-between" paddingLeft={1} paddingRight={1}>
|
|
179
|
+
<box width={22}>
|
|
180
|
+
<text fg={props.theme.primary} attributes={TextAttributes.BOLD}>
|
|
181
|
+
{props.label}
|
|
182
|
+
</text>
|
|
183
|
+
</box>
|
|
184
|
+
<text fg={accent} attributes={TextAttributes.BOLD}>
|
|
185
|
+
{props.value}
|
|
186
|
+
</text>
|
|
187
|
+
</box>
|
|
188
|
+
)
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
export function ActionRow(props: {
|
|
192
|
+
theme: Theme
|
|
193
|
+
title: string
|
|
194
|
+
detail: string
|
|
195
|
+
onClick: () => void
|
|
196
|
+
}) {
|
|
197
|
+
const accent = props.theme.primary
|
|
198
|
+
return (
|
|
199
|
+
<box
|
|
200
|
+
flexDirection="row"
|
|
201
|
+
justifyContent="space-between"
|
|
202
|
+
paddingLeft={1}
|
|
203
|
+
paddingRight={1}
|
|
204
|
+
onMouseUp={props.onClick}
|
|
205
|
+
>
|
|
206
|
+
<box flexDirection="row" gap={2}>
|
|
207
|
+
<box width={12}>
|
|
208
|
+
<text fg={accent} attributes={TextAttributes.BOLD}>
|
|
209
|
+
{props.title}
|
|
210
|
+
</text>
|
|
211
|
+
</box>
|
|
212
|
+
<text fg={props.theme.text}>{props.detail}</text>
|
|
213
|
+
</box>
|
|
214
|
+
<box paddingLeft={2} paddingRight={2} backgroundColor={accent}>
|
|
215
|
+
<text fg={props.theme.selectedListItemText}>open</text>
|
|
216
|
+
</box>
|
|
217
|
+
</box>
|
|
218
|
+
)
|
|
219
|
+
}
|
|
@@ -0,0 +1,347 @@
|
|
|
1
|
+
import type { Logger } from "../logger"
|
|
2
|
+
import type { SessionState } from "../state"
|
|
3
|
+
import {
|
|
4
|
+
formatPrunedItemsList,
|
|
5
|
+
formatProgressBar,
|
|
6
|
+
formatStatsHeader,
|
|
7
|
+
formatTokenCount,
|
|
8
|
+
} from "./utils"
|
|
9
|
+
import { ToolParameterEntry } from "../state"
|
|
10
|
+
import { PluginConfig } from "../config"
|
|
11
|
+
import { getActiveSummaryTokenUsage } from "../state/utils"
|
|
12
|
+
|
|
13
|
+
export type PruneReason = "completion" | "noise" | "extraction"
|
|
14
|
+
export const PRUNE_REASON_LABELS: Record<PruneReason, string> = {
|
|
15
|
+
completion: "Task Complete",
|
|
16
|
+
noise: "Noise Removal",
|
|
17
|
+
extraction: "Extraction",
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
interface CompressionNotificationEntry {
|
|
21
|
+
blockId: number
|
|
22
|
+
runId: number
|
|
23
|
+
summary: string
|
|
24
|
+
summaryTokens: number
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function buildMinimalMessage(state: SessionState, reason: PruneReason | undefined): string {
|
|
28
|
+
const reasonSuffix = reason ? ` — ${PRUNE_REASON_LABELS[reason]}` : ""
|
|
29
|
+
return (
|
|
30
|
+
formatStatsHeader(state.stats.totalPruneTokens, state.stats.pruneTokenCounter) +
|
|
31
|
+
reasonSuffix
|
|
32
|
+
)
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function buildDetailedMessage(
|
|
36
|
+
state: SessionState,
|
|
37
|
+
reason: PruneReason | undefined,
|
|
38
|
+
pruneToolIds: string[],
|
|
39
|
+
toolMetadata: Map<string, ToolParameterEntry>,
|
|
40
|
+
workingDirectory: string,
|
|
41
|
+
): string {
|
|
42
|
+
let message = formatStatsHeader(state.stats.totalPruneTokens, state.stats.pruneTokenCounter)
|
|
43
|
+
|
|
44
|
+
if (pruneToolIds.length > 0) {
|
|
45
|
+
const pruneTokenCounterStr = `~${formatTokenCount(state.stats.pruneTokenCounter)}`
|
|
46
|
+
const reasonLabel = reason ? ` — ${PRUNE_REASON_LABELS[reason]}` : ""
|
|
47
|
+
message += `\n\n▣ Pruning (${pruneTokenCounterStr})${reasonLabel}`
|
|
48
|
+
|
|
49
|
+
const itemLines = formatPrunedItemsList(pruneToolIds, toolMetadata, workingDirectory)
|
|
50
|
+
message += "\n" + itemLines.join("\n")
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
return message.trim()
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const TOAST_BODY_MAX_LINES = 12
|
|
57
|
+
const TOAST_SUMMARY_MAX_CHARS = 600
|
|
58
|
+
|
|
59
|
+
function truncateToastBody(body: string, maxLines: number = TOAST_BODY_MAX_LINES): string {
|
|
60
|
+
const lines = body.split("\n")
|
|
61
|
+
if (lines.length <= maxLines) {
|
|
62
|
+
return body
|
|
63
|
+
}
|
|
64
|
+
const kept = lines.slice(0, maxLines - 1)
|
|
65
|
+
const remaining = lines.length - maxLines + 1
|
|
66
|
+
return kept.join("\n") + `\n... and ${remaining} more`
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function truncateToastSummary(summary: string, maxChars: number = TOAST_SUMMARY_MAX_CHARS): string {
|
|
70
|
+
if (summary.length <= maxChars) {
|
|
71
|
+
return summary
|
|
72
|
+
}
|
|
73
|
+
return summary.slice(0, maxChars - 3) + "..."
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function truncateExtractedSection(
|
|
77
|
+
message: string,
|
|
78
|
+
maxChars: number = TOAST_SUMMARY_MAX_CHARS,
|
|
79
|
+
): string {
|
|
80
|
+
const marker = "\n\n▣ Extracted"
|
|
81
|
+
const index = message.indexOf(marker)
|
|
82
|
+
if (index === -1) {
|
|
83
|
+
return message
|
|
84
|
+
}
|
|
85
|
+
const extracted = message.slice(index)
|
|
86
|
+
if (extracted.length <= maxChars) {
|
|
87
|
+
return message
|
|
88
|
+
}
|
|
89
|
+
return message.slice(0, index) + truncateToastSummary(extracted, maxChars)
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export async function sendUnifiedNotification(
|
|
93
|
+
client: any,
|
|
94
|
+
logger: Logger,
|
|
95
|
+
config: PluginConfig,
|
|
96
|
+
state: SessionState,
|
|
97
|
+
sessionId: string,
|
|
98
|
+
pruneToolIds: string[],
|
|
99
|
+
toolMetadata: Map<string, ToolParameterEntry>,
|
|
100
|
+
reason: PruneReason | undefined,
|
|
101
|
+
params: any,
|
|
102
|
+
workingDirectory: string,
|
|
103
|
+
): Promise<boolean> {
|
|
104
|
+
const hasPruned = pruneToolIds.length > 0
|
|
105
|
+
if (!hasPruned) {
|
|
106
|
+
return false
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
if (config.pruneNotification === "off") {
|
|
110
|
+
return false
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const message =
|
|
114
|
+
config.pruneNotification === "minimal"
|
|
115
|
+
? buildMinimalMessage(state, reason)
|
|
116
|
+
: buildDetailedMessage(state, reason, pruneToolIds, toolMetadata, workingDirectory)
|
|
117
|
+
|
|
118
|
+
if (config.pruneNotificationType === "toast") {
|
|
119
|
+
let toastMessage = truncateExtractedSection(message)
|
|
120
|
+
toastMessage =
|
|
121
|
+
config.pruneNotification === "minimal" ? toastMessage : truncateToastBody(toastMessage)
|
|
122
|
+
|
|
123
|
+
await client.tui.showToast({
|
|
124
|
+
body: {
|
|
125
|
+
title: "DCP: Compress Notification",
|
|
126
|
+
message: toastMessage,
|
|
127
|
+
variant: "info",
|
|
128
|
+
duration: 5000,
|
|
129
|
+
},
|
|
130
|
+
})
|
|
131
|
+
return true
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
await sendIgnoredMessage(client, sessionId, message, params, logger)
|
|
135
|
+
return true
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function buildCompressionSummary(
|
|
139
|
+
entries: CompressionNotificationEntry[],
|
|
140
|
+
state: SessionState,
|
|
141
|
+
): string {
|
|
142
|
+
if (entries.length === 1) {
|
|
143
|
+
return entries[0]?.summary ?? ""
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
return entries
|
|
147
|
+
.map((entry) => {
|
|
148
|
+
const topic =
|
|
149
|
+
state.prune.messages.blocksById.get(entry.blockId)?.topic ?? "(unknown topic)"
|
|
150
|
+
return `### ${topic}\n${entry.summary}`
|
|
151
|
+
})
|
|
152
|
+
.join("\n\n")
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function getCompressionLabel(entries: CompressionNotificationEntry[]): string {
|
|
156
|
+
const runId = entries[0]?.runId
|
|
157
|
+
if (runId === undefined) {
|
|
158
|
+
return "Compression"
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
return `Compression #${runId}`
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
function formatCompressionMetrics(removedTokens: number, summaryTokens: number): string {
|
|
165
|
+
const metrics = [`-${formatTokenCount(removedTokens, true)} removed`]
|
|
166
|
+
if (summaryTokens > 0) {
|
|
167
|
+
metrics.push(`+${formatTokenCount(summaryTokens, true)} summary`)
|
|
168
|
+
}
|
|
169
|
+
return metrics.join(", ")
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
export async function sendCompressNotification(
|
|
173
|
+
client: any,
|
|
174
|
+
logger: Logger,
|
|
175
|
+
config: PluginConfig,
|
|
176
|
+
state: SessionState,
|
|
177
|
+
sessionId: string,
|
|
178
|
+
entries: CompressionNotificationEntry[],
|
|
179
|
+
batchTopic: string | undefined,
|
|
180
|
+
sessionMessageIds: string[],
|
|
181
|
+
params: any,
|
|
182
|
+
): Promise<boolean> {
|
|
183
|
+
if (config.pruneNotification === "off") {
|
|
184
|
+
return false
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
if (entries.length === 0) {
|
|
188
|
+
return false
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
let message: string
|
|
192
|
+
const compressionLabel = getCompressionLabel(entries)
|
|
193
|
+
const summary = buildCompressionSummary(entries, state)
|
|
194
|
+
const summaryTokens = entries.reduce((total, entry) => total + entry.summaryTokens, 0)
|
|
195
|
+
const summaryTokensStr = formatTokenCount(summaryTokens)
|
|
196
|
+
const compressedTokens = entries.reduce((total, entry) => {
|
|
197
|
+
const compressionBlock = state.prune.messages.blocksById.get(entry.blockId)
|
|
198
|
+
if (!compressionBlock) {
|
|
199
|
+
logger.error("Compression block missing for notification", {
|
|
200
|
+
compressionId: entry.blockId,
|
|
201
|
+
sessionId,
|
|
202
|
+
})
|
|
203
|
+
return total
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
return total + compressionBlock.compressedTokens
|
|
207
|
+
}, 0)
|
|
208
|
+
|
|
209
|
+
const newlyCompressedMessageIds: string[] = []
|
|
210
|
+
const newlyCompressedToolIds: string[] = []
|
|
211
|
+
const seenMessageIds = new Set<string>()
|
|
212
|
+
const seenToolIds = new Set<string>()
|
|
213
|
+
|
|
214
|
+
for (const entry of entries) {
|
|
215
|
+
const compressionBlock = state.prune.messages.blocksById.get(entry.blockId)
|
|
216
|
+
if (!compressionBlock) {
|
|
217
|
+
continue
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
for (const messageId of compressionBlock.directMessageIds) {
|
|
221
|
+
if (seenMessageIds.has(messageId)) {
|
|
222
|
+
continue
|
|
223
|
+
}
|
|
224
|
+
seenMessageIds.add(messageId)
|
|
225
|
+
newlyCompressedMessageIds.push(messageId)
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
for (const toolId of compressionBlock.directToolIds) {
|
|
229
|
+
if (seenToolIds.has(toolId)) {
|
|
230
|
+
continue
|
|
231
|
+
}
|
|
232
|
+
seenToolIds.add(toolId)
|
|
233
|
+
newlyCompressedToolIds.push(toolId)
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
const topic =
|
|
238
|
+
batchTopic ??
|
|
239
|
+
(entries.length === 1
|
|
240
|
+
? (state.prune.messages.blocksById.get(entries[0]?.blockId ?? -1)?.topic ??
|
|
241
|
+
"(unknown topic)")
|
|
242
|
+
: "(unknown topic)")
|
|
243
|
+
|
|
244
|
+
const totalActiveSummaryTkns = getActiveSummaryTokenUsage(state)
|
|
245
|
+
const totalGross = state.stats.totalPruneTokens + state.stats.pruneTokenCounter
|
|
246
|
+
const notificationHeader = `▣ DCP | ${formatCompressionMetrics(totalGross, totalActiveSummaryTkns)}`
|
|
247
|
+
|
|
248
|
+
if (config.pruneNotification === "minimal") {
|
|
249
|
+
message = `${notificationHeader} — ${compressionLabel}`
|
|
250
|
+
} else {
|
|
251
|
+
message = notificationHeader
|
|
252
|
+
|
|
253
|
+
const activePrunedMessages = new Map<string, number>()
|
|
254
|
+
for (const [messageId, entry] of state.prune.messages.byMessageId) {
|
|
255
|
+
if (entry.activeBlockIds.length > 0) {
|
|
256
|
+
activePrunedMessages.set(messageId, entry.tokenCount)
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
const progressBar = formatProgressBar(
|
|
260
|
+
sessionMessageIds,
|
|
261
|
+
activePrunedMessages,
|
|
262
|
+
newlyCompressedMessageIds,
|
|
263
|
+
50,
|
|
264
|
+
)
|
|
265
|
+
message += `\n\n${progressBar}`
|
|
266
|
+
message += `\n▣ ${compressionLabel} ${formatCompressionMetrics(compressedTokens, summaryTokens)}`
|
|
267
|
+
message += `\n→ Topic: ${topic}`
|
|
268
|
+
message += `\n→ Items: ${newlyCompressedMessageIds.length} messages`
|
|
269
|
+
if (newlyCompressedToolIds.length > 0) {
|
|
270
|
+
message += ` and ${newlyCompressedToolIds.length} tools compressed`
|
|
271
|
+
} else {
|
|
272
|
+
message += ` compressed`
|
|
273
|
+
}
|
|
274
|
+
if (config.compress.showCompression) {
|
|
275
|
+
message += `\n→ Compression (~${summaryTokensStr}): ${summary}`
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
if (config.pruneNotificationType === "toast") {
|
|
280
|
+
let toastMessage = message
|
|
281
|
+
if (config.compress.showCompression) {
|
|
282
|
+
const truncatedSummary = truncateToastSummary(summary)
|
|
283
|
+
if (truncatedSummary !== summary) {
|
|
284
|
+
toastMessage = toastMessage.replace(
|
|
285
|
+
`\n→ Compression (~${summaryTokensStr}): ${summary}`,
|
|
286
|
+
`\n→ Compression (~${summaryTokensStr}): ${truncatedSummary}`,
|
|
287
|
+
)
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
toastMessage =
|
|
291
|
+
config.pruneNotification === "minimal" ? toastMessage : truncateToastBody(toastMessage)
|
|
292
|
+
|
|
293
|
+
await client.tui.showToast({
|
|
294
|
+
body: {
|
|
295
|
+
title: "DCP: Compress Notification",
|
|
296
|
+
message: toastMessage,
|
|
297
|
+
variant: "info",
|
|
298
|
+
duration: 5000,
|
|
299
|
+
},
|
|
300
|
+
})
|
|
301
|
+
return true
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
await sendIgnoredMessage(client, sessionId, message, params, logger)
|
|
305
|
+
return true
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
export async function sendIgnoredMessage(
|
|
309
|
+
client: any,
|
|
310
|
+
sessionID: string,
|
|
311
|
+
text: string,
|
|
312
|
+
params: any,
|
|
313
|
+
logger: Logger,
|
|
314
|
+
): Promise<void> {
|
|
315
|
+
const agent = params.agent || undefined
|
|
316
|
+
const variant = params.variant || undefined
|
|
317
|
+
const model =
|
|
318
|
+
params.providerId && params.modelId
|
|
319
|
+
? {
|
|
320
|
+
providerID: params.providerId,
|
|
321
|
+
modelID: params.modelId,
|
|
322
|
+
}
|
|
323
|
+
: undefined
|
|
324
|
+
|
|
325
|
+
try {
|
|
326
|
+
await client.session.prompt({
|
|
327
|
+
path: {
|
|
328
|
+
id: sessionID,
|
|
329
|
+
},
|
|
330
|
+
body: {
|
|
331
|
+
noReply: true,
|
|
332
|
+
agent: agent,
|
|
333
|
+
model: model,
|
|
334
|
+
variant: variant,
|
|
335
|
+
parts: [
|
|
336
|
+
{
|
|
337
|
+
type: "text",
|
|
338
|
+
text: text,
|
|
339
|
+
ignored: true,
|
|
340
|
+
},
|
|
341
|
+
],
|
|
342
|
+
},
|
|
343
|
+
})
|
|
344
|
+
} catch (error: any) {
|
|
345
|
+
logger.error("Failed to send notification", { error: error.message })
|
|
346
|
+
}
|
|
347
|
+
}
|