Skip to content

Commit 14bc646

Browse files
committed
feat(tui): load tui config from dcp.jsonc
Unify TUI behavior with file-based plugin config and ship the TUI entrypoint so sidebar updates actually reach users.
1 parent 1bf5d9b commit 14bc646

18 files changed

Lines changed: 3002 additions & 421 deletions

dcp.schema.json

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,27 @@
9494
}
9595
}
9696
},
97+
"tui": {
98+
"type": "object",
99+
"description": "Configuration for the DCP TUI integration",
100+
"additionalProperties": false,
101+
"properties": {
102+
"sidebar": {
103+
"type": "boolean",
104+
"default": true,
105+
"description": "Show the DCP sidebar widget in the TUI"
106+
},
107+
"debug": {
108+
"type": "boolean",
109+
"default": false,
110+
"description": "Enable debug/error logging for the DCP TUI"
111+
}
112+
},
113+
"default": {
114+
"sidebar": true,
115+
"debug": false
116+
}
117+
},
97118
"experimental": {
98119
"type": "object",
99120
"description": "Experimental settings that may change in future releases",

index.ts

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { Logger } from "./lib/logger"
44
import { createSessionState } from "./lib/state"
55
import { createCompressTool } from "./lib/tools"
66
import { PromptStore } from "./lib/prompts/store"
7+
import tuiPlugin from "./tui/index"
78
import {
89
createChatMessageTransformHandler,
910
createCommandExecuteHandler,
@@ -12,7 +13,7 @@ import {
1213
} from "./lib/hooks"
1314
import { configureClientAuth, isSecureMode } from "./lib/auth"
1415

15-
const plugin: Plugin = (async (ctx) => {
16+
const server: Plugin = (async (ctx) => {
1617
const config = getConfig(ctx)
1718

1819
if (!config.enabled) {
@@ -114,4 +115,7 @@ const plugin: Plugin = (async (ctx) => {
114115
}
115116
}) satisfies Plugin
116117

117-
export default plugin
118+
export default {
119+
server,
120+
...tuiPlugin,
121+
}

lib/config.ts

Lines changed: 119 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,11 @@ export interface TurnProtection {
5151
turns: number
5252
}
5353

54+
export interface TuiConfig {
55+
sidebar: boolean
56+
debug: boolean
57+
}
58+
5459
export interface ExperimentalConfig {
5560
allowSubAgents: boolean
5661
customPrompts: boolean
@@ -64,6 +69,7 @@ export interface PluginConfig {
6469
commands: Commands
6570
manualMode: ManualModeConfig
6671
turnProtection: TurnProtection
72+
tui: TuiConfig
6773
experimental: ExperimentalConfig
6874
protectedFilePatterns: string[]
6975
compress: CompressTool
@@ -101,6 +107,9 @@ export const VALID_CONFIG_KEYS = new Set([
101107
"turnProtection",
102108
"turnProtection.enabled",
103109
"turnProtection.turns",
110+
"tui",
111+
"tui.sidebar",
112+
"tui.debug",
104113
"experimental",
105114
"experimental.allowSubAgents",
106115
"experimental.customPrompts",
@@ -165,6 +174,13 @@ interface ValidationError {
165174
actual: string
166175
}
167176

177+
type ConfigWarningNotifier = (title: string, message: string) => void
178+
179+
interface ConfigWarningCallbacks {
180+
onParseWarning?: (title: string, message: string) => void
181+
onConfigWarning?: (configPath: string, data: Record<string, any>, isProject: boolean) => void
182+
}
183+
168184
export function validateConfigTypes(config: Record<string, any>): ValidationError[] {
169185
const errors: ValidationError[] = []
170186

@@ -282,6 +298,32 @@ export function validateConfigTypes(config: Record<string, any>): ValidationErro
282298
}
283299
}
284300

301+
const tui = config.tui
302+
if (tui !== undefined) {
303+
if (typeof tui !== "object" || tui === null || Array.isArray(tui)) {
304+
errors.push({
305+
key: "tui",
306+
expected: "object",
307+
actual: typeof tui,
308+
})
309+
} else {
310+
if (tui.sidebar !== undefined && typeof tui.sidebar !== "boolean") {
311+
errors.push({
312+
key: "tui.sidebar",
313+
expected: "boolean",
314+
actual: typeof tui.sidebar,
315+
})
316+
}
317+
if (tui.debug !== undefined && typeof tui.debug !== "boolean") {
318+
errors.push({
319+
key: "tui.debug",
320+
expected: "boolean",
321+
actual: typeof tui.debug,
322+
})
323+
}
324+
}
325+
}
326+
285327
const commands = config.commands
286328
if (commands !== undefined) {
287329
if (typeof commands !== "object" || commands === null || Array.isArray(commands)) {
@@ -594,8 +636,21 @@ export function validateConfigTypes(config: Record<string, any>): ValidationErro
594636
return errors
595637
}
596638

639+
function scheduleConfigWarning(
640+
notify: ConfigWarningNotifier | undefined,
641+
title: string,
642+
message: string,
643+
): void {
644+
setTimeout(() => {
645+
if (!notify) return
646+
try {
647+
notify(title, message)
648+
} catch {}
649+
}, 7000)
650+
}
651+
597652
function showConfigWarnings(
598-
ctx: PluginInput,
653+
notify: ConfigWarningNotifier | undefined,
599654
configPath: string,
600655
configData: Record<string, any>,
601656
isProject: boolean,
@@ -625,18 +680,11 @@ function showConfigWarnings(
625680
}
626681
}
627682

628-
setTimeout(() => {
629-
try {
630-
ctx.client.tui.showToast({
631-
body: {
632-
title: `DCP: ${configType} warning`,
633-
message: `${configPath}\n${messages.join("\n")}`,
634-
variant: "warning",
635-
duration: 7000,
636-
},
637-
})
638-
} catch {}
639-
}, 7000)
683+
scheduleConfigWarning(
684+
notify,
685+
`DCP: ${configType} warning`,
686+
`${configPath}\n${messages.join("\n")}`,
687+
)
640688
}
641689

642690
const defaultConfig: PluginConfig = {
@@ -652,6 +700,10 @@ const defaultConfig: PluginConfig = {
652700
enabled: false,
653701
automaticStrategies: true,
654702
},
703+
tui: {
704+
sidebar: true,
705+
debug: false,
706+
},
655707
turnProtection: {
656708
enabled: false,
657709
turns: 4,
@@ -711,7 +763,7 @@ function findOpencodeDir(startDir: string): string | null {
711763
return null
712764
}
713765

714-
function getConfigPaths(ctx?: PluginInput): {
766+
function getConfigPaths(directory?: string): {
715767
global: string | null
716768
configDir: string | null
717769
project: string | null
@@ -735,8 +787,8 @@ function getConfigPaths(ctx?: PluginInput): {
735787
}
736788

737789
let project: string | null = null
738-
if (ctx?.directory) {
739-
const opencodeDir = findOpencodeDir(ctx.directory)
790+
if (directory) {
791+
const opencodeDir = findOpencodeDir(directory)
740792
if (opencodeDir) {
741793
const projectJsonc = join(opencodeDir, "dcp.jsonc")
742794
const projectJson = join(opencodeDir, "dcp.json")
@@ -871,6 +923,18 @@ function mergeManualMode(
871923
}
872924
}
873925

926+
function mergeTui(
927+
base: PluginConfig["tui"],
928+
override?: Partial<PluginConfig["tui"]>,
929+
): PluginConfig["tui"] {
930+
if (override === undefined) return base
931+
932+
return {
933+
sidebar: override.sidebar ?? base.sidebar,
934+
debug: override.debug ?? base.debug,
935+
}
936+
}
937+
874938
function mergeExperimental(
875939
base: PluginConfig["experimental"],
876940
override?: Partial<PluginConfig["experimental"]>,
@@ -894,6 +958,7 @@ function deepCloneConfig(config: PluginConfig): PluginConfig {
894958
enabled: config.manualMode.enabled,
895959
automaticStrategies: config.manualMode.automaticStrategies,
896960
},
961+
tui: { ...config.tui },
897962
turnProtection: { ...config.turnProtection },
898963
experimental: { ...config.experimental },
899964
protectedFilePatterns: [...config.protectedFilePatterns],
@@ -923,6 +988,7 @@ function mergeLayer(config: PluginConfig, data: Record<string, any>): PluginConf
923988
debug: data.debug ?? config.debug,
924989
pruneNotification: data.pruneNotification ?? config.pruneNotification,
925990
pruneNotificationType: data.pruneNotificationType ?? config.pruneNotificationType,
991+
tui: mergeTui(config.tui, data.tui as any),
926992
commands: mergeCommands(config.commands, data.commands as any),
927993
manualMode: mergeManualMode(config.manualMode, data.manualMode as any),
928994
turnProtection: {
@@ -938,24 +1004,21 @@ function mergeLayer(config: PluginConfig, data: Record<string, any>): PluginConf
9381004
}
9391005
}
9401006

941-
function scheduleParseWarning(ctx: PluginInput, title: string, message: string): void {
942-
setTimeout(() => {
943-
try {
944-
ctx.client.tui.showToast({
945-
body: {
946-
title,
947-
message,
948-
variant: "warning",
949-
duration: 7000,
950-
},
951-
})
952-
} catch {}
953-
}, 7000)
1007+
function createConfigWarningCallbacks(
1008+
notify?: ConfigWarningNotifier,
1009+
): ConfigWarningCallbacks | undefined {
1010+
if (!notify) return undefined
1011+
1012+
return {
1013+
onParseWarning: (title, message) => scheduleConfigWarning(notify, title, message),
1014+
onConfigWarning: (configPath, data, isProject) =>
1015+
showConfigWarnings(notify, configPath, data, isProject),
1016+
}
9541017
}
9551018

956-
export function getConfig(ctx: PluginInput): PluginConfig {
1019+
function loadMergedConfig(directory?: string, callbacks?: ConfigWarningCallbacks): PluginConfig {
9571020
let config = deepCloneConfig(defaultConfig)
958-
const configPaths = getConfigPaths(ctx)
1021+
const configPaths = getConfigPaths(directory)
9591022

9601023
if (!configPaths.global) {
9611024
createDefaultConfig()
@@ -974,8 +1037,7 @@ export function getConfig(ctx: PluginInput): PluginConfig {
9741037

9751038
const result = loadConfigFile(layer.path)
9761039
if (result.parseError) {
977-
scheduleParseWarning(
978-
ctx,
1040+
callbacks?.onParseWarning?.(
9791041
`DCP: Invalid ${layer.name}`,
9801042
`${layer.path}\n${result.parseError}\nUsing previous/default values`,
9811043
)
@@ -986,9 +1048,32 @@ export function getConfig(ctx: PluginInput): PluginConfig {
9861048
continue
9871049
}
9881050

989-
showConfigWarnings(ctx, layer.path, result.data, layer.isProject)
1051+
callbacks?.onConfigWarning?.(layer.path, result.data, layer.isProject)
9901052
config = mergeLayer(config, result.data)
9911053
}
9921054

9931055
return config
9941056
}
1057+
1058+
export function getConfigForDirectory(
1059+
directory?: string,
1060+
notify?: ConfigWarningNotifier,
1061+
): PluginConfig {
1062+
return loadMergedConfig(directory, createConfigWarningCallbacks(notify))
1063+
}
1064+
1065+
export function getConfig(ctx: PluginInput): PluginConfig {
1066+
return loadMergedConfig(
1067+
ctx.directory,
1068+
createConfigWarningCallbacks((title, message) => {
1069+
ctx.client.tui.showToast({
1070+
body: {
1071+
title,
1072+
message,
1073+
variant: "warning",
1074+
duration: 7000,
1075+
},
1076+
})
1077+
}),
1078+
)
1079+
}

0 commit comments

Comments
 (0)