-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathsettings.tsx
More file actions
181 lines (160 loc) · 6.07 KB
/
Copy pathsettings.tsx
File metadata and controls
181 lines (160 loc) · 6.07 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
/**
* The indexer's panel in Settings (`settings-tab`).
*
* Deliberately small. Every *setting* — provider, model, store URL and the seven
* credentials — is declared in `plugin.json`'s `config` schema, which the Plugins panel
* already renders as a form (with password fields for the `secret: true` ones). What that
* generic form cannot do is show what the index is DOING and let the user act on it, so
* that is all this adds: state, progress, and the four buttons.
*
* The 990-line bespoke config form this replaces existed because the settings lived in
* core's global state and needed hand-written plumbing per provider; as plugin config they
* need none.
*
* BUILD: `ui/settings.js` (esbuild ESM; React and `@shofer/plugin-ui` external).
*/
import { useCallback, useEffect, useState } from "react"
import { Badge, Button, ToggleSwitch, cn, usePluginTranslation } from "@shofer/plugin-ui"
/** The restricted API the host hands a plugin UI component (`PluginUIApi` in `@shofer/types`). */
interface PluginUIApi {
postMessage(message: unknown): void
onMessage(listener: (message: unknown) => void): () => void
request(method: string, params?: unknown, opts?: { mutates?: boolean }): Promise<unknown>
readonly context: { readonly region: string; readonly pluginName: string }
}
/** One index's state, as `manager.getCurrentStatus()` reports it. */
interface IndexStatus {
systemStatus: string
message?: string
processedItems?: number
totalItems?: number
currentItemUnit?: string
workspacePath?: string
workspaceEnabled?: boolean
autoEnableDefault?: boolean
}
interface StatusReply {
code?: IndexStatus
git?: IndexStatus
}
/** How often the panel re-reads state while it is open. */
const POLL_MS = 2000
const TONE: Record<string, string> = {
Indexed: "text-vscode-charts-green",
Indexing: "text-vscode-charts-blue",
Error: "text-vscode-errorForeground",
Standby: "text-vscode-descriptionForeground",
Disabled: "text-vscode-descriptionForeground",
}
function ask<T>(api: PluginUIApi, method: string, params?: unknown, mutates = false): Promise<T> {
// `local:` — the index lives on the machine with the workspace, so these questions are
// answered here even while a remote executor's task is focused.
return api.request(`local:${method}`, params, { mutates }) as Promise<T>
}
function Progress({ status }: { status: IndexStatus }) {
const t = usePluginTranslation()
if (status.systemStatus !== "Indexing" || !status.totalItems) return null
const percent = Math.min(100, Math.round(((status.processedItems ?? 0) / status.totalItems) * 100))
return (
<div className="text-xs text-vscode-descriptionForeground">
{t("panel.progress", {
processed: status.processedItems ?? 0,
total: status.totalItems,
unit: status.currentItemUnit ?? "items",
percent,
})}
</div>
)
}
export default function RagIndexingSettings({ api }: { api: PluginUIApi }): React.JSX.Element {
const t = usePluginTranslation()
const [status, setStatus] = useState<StatusReply | null>(null)
const [busy, setBusy] = useState(false)
const [error, setError] = useState<string | null>(null)
const refresh = useCallback(() => {
void ask<StatusReply>(api, "status")
.then((next) => {
setStatus(next)
setError(null)
})
.catch((refreshError: unknown) =>
setError(refreshError instanceof Error ? refreshError.message : String(refreshError)),
)
}, [api])
useEffect(() => {
refresh()
const timer = setInterval(refresh, POLL_MS)
return () => clearInterval(timer)
}, [refresh])
const act = useCallback(
async (method: string, params?: unknown) => {
setBusy(true)
setError(null)
try {
await ask(api, method, params, true)
} catch (actionError) {
setError(actionError instanceof Error ? actionError.message : String(actionError))
} finally {
setBusy(false)
refresh()
}
},
[api, refresh],
)
const code = status?.code
const git = status?.git
return (
<div className="flex flex-col gap-3 px-5 py-3">
<div>
<h4 className="text-sm font-semibold m-0">{t("panel.title")}</h4>
<p className="text-xs text-vscode-descriptionForeground m-0 mt-1">{t("panel.description")}</p>
</div>
<div className="flex items-center gap-2">
<span className="text-sm">{t("panel.codeIndex")}</span>
<Badge variant="secondary" className={cn("text-[0.7em]", TONE[code?.systemStatus ?? "Standby"])}>
{code?.systemStatus ?? t("panel.unknown")}
</Badge>
{code?.message && <span className="text-xs text-vscode-descriptionForeground">{code.message}</span>}
</div>
{code && <Progress status={code} />}
<div className="flex flex-wrap gap-2">
<Button
variant="secondary"
size="sm"
disabled={busy || code?.systemStatus === "Indexing"}
onClick={() => void act("start-indexing")}>
{t("panel.startIndexing")}
</Button>
<Button
variant="secondary"
size="sm"
disabled={busy || code?.systemStatus !== "Indexing"}
onClick={() => void act("stop-indexing")}>
{t("panel.stopIndexing")}
</Button>
<Button variant="destructive" size="sm" disabled={busy} onClick={() => void act("clear-index")}>
{t("panel.clearIndex")}
</Button>
</div>
<label className="flex items-center gap-2 text-sm">
<ToggleSwitch
checked={code?.workspaceEnabled !== false}
size="small"
aria-label={t("panel.workspaceEnabled")}
onChange={(enabled) => void act("set-workspace-enabled", { enabled })}
/>
<span>{t("panel.workspaceEnabled")}</span>
</label>
<div className="flex items-center gap-2 border-t border-vscode-panel-border pt-3">
<span className="text-sm">{t("panel.gitIndex")}</span>
<Badge variant="secondary" className={cn("text-[0.7em]", TONE[git?.systemStatus ?? "Standby"])}>
{git?.systemStatus ?? t("panel.unknown")}
</Badge>
{git?.message && <span className="text-xs text-vscode-descriptionForeground">{git.message}</span>}
</div>
{git && <Progress status={git} />}
<p className="text-xs text-vscode-descriptionForeground m-0">{t("panel.configureHint")}</p>
{error && <div className="text-xs text-vscode-errorForeground">{error}</div>}
</div>
)
}