pstack-dsh 0.2.3

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.
Files changed (172) hide show
  1. package/HARNESS.md +109 -0
  2. package/LICENSE +25 -0
  3. package/README.md +168 -0
  4. package/TEST-PLAN.md +70 -0
  5. package/agents/comment-sicko.md +32 -0
  6. package/agents/poteto-agent.md +9 -0
  7. package/cordis.patch.yml +4 -0
  8. package/cordis.yml +4 -0
  9. package/docs/guide/01-setup.md +41 -0
  10. package/docs/guide/02-poteto-mode.md +98 -0
  11. package/docs/guide/03-understand.md +53 -0
  12. package/docs/guide/04-design.md +81 -0
  13. package/docs/guide/05-build-and-clean.md +75 -0
  14. package/docs/guide/06-verify-and-ship.md +87 -0
  15. package/docs/guide/07-overnight.md +81 -0
  16. package/docs/guide/08-principles.md +71 -0
  17. package/docs/guide/09-make-it-yours.md +67 -0
  18. package/docs/guide/10-recipes-and-pitfalls.md +96 -0
  19. package/docs/guide/README.md +30 -0
  20. package/dshx.yml +9 -0
  21. package/lib/client.js +981 -0
  22. package/lib/client.js.map +1 -0
  23. package/lib/index.d.ts +34 -0
  24. package/lib/index.js +1120 -0
  25. package/package.json +140 -0
  26. package/skills/architect/SKILL.md +83 -0
  27. package/skills/architect/references/design-red-flags.md +33 -0
  28. package/skills/architect/references/rationale-template.md +35 -0
  29. package/skills/architect/references/runner-prompt.md +20 -0
  30. package/skills/arena/SKILL.md +73 -0
  31. package/skills/automate-me/SKILL.md +103 -0
  32. package/skills/blast-radius/SKILL.md +50 -0
  33. package/skills/bro/SKILL.md +7 -0
  34. package/skills/create-verification-skill/SKILL.md +44 -0
  35. package/skills/create-verification-skill/references/feature-map-example/README.md +47 -0
  36. package/skills/create-verification-skill/references/feature-map-example/create-note.md +39 -0
  37. package/skills/create-verification-skill/references/feature-map-example/search.md +45 -0
  38. package/skills/figure-it-out/SKILL.md +53 -0
  39. package/skills/how/SKILL.md +45 -0
  40. package/skills/how/references/explainer-prompt.md +55 -0
  41. package/skills/how/references/explorer-prompt.md +52 -0
  42. package/skills/interrogate/SKILL.md +99 -0
  43. package/skills/interrogate/references/code-quality-review.md +47 -0
  44. package/skills/interrogate/references/lead-judgment.md +58 -0
  45. package/skills/interrogate/references/reviewer-prompt.md +72 -0
  46. package/skills/interrogate/references/rubric.md +77 -0
  47. package/skills/maintain-verification-skill/SKILL.md +39 -0
  48. package/skills/no-comments/SKILL.md +24 -0
  49. package/skills/poteto-mode/SKILL.md +149 -0
  50. package/skills/poteto-mode/playbooks/authoring-a-skill.md +12 -0
  51. package/skills/poteto-mode/playbooks/autonomous-run.md +13 -0
  52. package/skills/poteto-mode/playbooks/autopilot-full.md +13 -0
  53. package/skills/poteto-mode/playbooks/autopilot-stack.md +16 -0
  54. package/skills/poteto-mode/playbooks/babysit.md +27 -0
  55. package/skills/poteto-mode/playbooks/bug-fix.md +17 -0
  56. package/skills/poteto-mode/playbooks/eval.md +25 -0
  57. package/skills/poteto-mode/playbooks/feature.md +21 -0
  58. package/skills/poteto-mode/playbooks/hillclimb.md +21 -0
  59. package/skills/poteto-mode/playbooks/investigation.md +14 -0
  60. package/skills/poteto-mode/playbooks/multi-phase-plan.md +156 -0
  61. package/skills/poteto-mode/playbooks/opening-a-pr.md +33 -0
  62. package/skills/poteto-mode/playbooks/orchestrate.md +113 -0
  63. package/skills/poteto-mode/playbooks/pause-safely.md +10 -0
  64. package/skills/poteto-mode/playbooks/perf-issue.md +24 -0
  65. package/skills/poteto-mode/playbooks/prototype.md +14 -0
  66. package/skills/poteto-mode/playbooks/refactoring.md +16 -0
  67. package/skills/poteto-mode/playbooks/runtime-forensics.md +11 -0
  68. package/skills/poteto-mode/playbooks/session-pickup.md +11 -0
  69. package/skills/poteto-mode/playbooks/shipping.md +17 -0
  70. package/skills/poteto-mode/playbooks/trace-forensics.md +14 -0
  71. package/skills/poteto-mode/playbooks/visual-parity.md +11 -0
  72. package/skills/poteto-mode/playbooks/worktree-cleanup.md +14 -0
  73. package/skills/poteto-mode/references/bugbot-triage.md +142 -0
  74. package/skills/poteto-mode/scripts/bootstrap.ts +62 -0
  75. package/skills/poteto-mode/scripts/bun.lock +67 -0
  76. package/skills/poteto-mode/scripts/check-plan.mjs +186 -0
  77. package/skills/poteto-mode/scripts/orch/orch.test.ts +634 -0
  78. package/skills/poteto-mode/scripts/orch/orch.ts +578 -0
  79. package/skills/poteto-mode/scripts/orch/store.ts +1607 -0
  80. package/skills/poteto-mode/scripts/package.json +16 -0
  81. package/skills/poteto-mode/scripts/watch-pr/cli.test.ts +224 -0
  82. package/skills/poteto-mode/scripts/watch-pr/cli.ts +223 -0
  83. package/skills/poteto-mode/scripts/watch-pr/fakes.test-helper.ts +118 -0
  84. package/skills/poteto-mode/scripts/watch-pr/github.test.ts +306 -0
  85. package/skills/poteto-mode/scripts/watch-pr/github.ts +699 -0
  86. package/skills/poteto-mode/scripts/watch-pr/policy.test.ts +420 -0
  87. package/skills/poteto-mode/scripts/watch-pr/policy.ts +832 -0
  88. package/skills/poteto-mode/scripts/watch-pr/render.ts +169 -0
  89. package/skills/poteto-mode/scripts/watch-pr/tsconfig.json +13 -0
  90. package/skills/poteto-mode/scripts/watch-pr/types.compile.ts +93 -0
  91. package/skills/poteto-mode/scripts/watch-pr/types.ts +401 -0
  92. package/skills/poteto-mode/scripts/watch-pr/watch-pr +6 -0
  93. package/skills/poteto-mode/scripts/worktree-audit.sh +86 -0
  94. package/skills/principle-attack-the-premise/SKILL.md +22 -0
  95. package/skills/principle-boundary-discipline/SKILL.md +33 -0
  96. package/skills/principle-build-the-lever/SKILL.md +22 -0
  97. package/skills/principle-encode-lessons-in-structure/SKILL.md +30 -0
  98. package/skills/principle-exhaust-the-design-space/SKILL.md +20 -0
  99. package/skills/principle-experience-first/SKILL.md +18 -0
  100. package/skills/principle-fix-root-causes/SKILL.md +22 -0
  101. package/skills/principle-foundational-thinking/SKILL.md +20 -0
  102. package/skills/principle-guard-the-context-window/SKILL.md +16 -0
  103. package/skills/principle-laziness-protocol/SKILL.md +17 -0
  104. package/skills/principle-make-operations-idempotent/SKILL.md +23 -0
  105. package/skills/principle-migrate-callers-then-delete-legacy-apis/SKILL.md +21 -0
  106. package/skills/principle-minimize-reader-load/SKILL.md +22 -0
  107. package/skills/principle-model-the-domain/SKILL.md +25 -0
  108. package/skills/principle-never-block-on-the-human/SKILL.md +21 -0
  109. package/skills/principle-outcome-oriented-execution/SKILL.md +21 -0
  110. package/skills/principle-prove-it-works/SKILL.md +32 -0
  111. package/skills/principle-redesign-from-first-principles/SKILL.md +15 -0
  112. package/skills/principle-separate-before-serializing-shared-state/SKILL.md +15 -0
  113. package/skills/principle-sequence-verifiable-units/SKILL.md +21 -0
  114. package/skills/principle-subtract-before-you-add/SKILL.md +20 -0
  115. package/skills/principle-test-behavior-not-implementation/SKILL.md +24 -0
  116. package/skills/principle-type-system-discipline/SKILL.md +30 -0
  117. package/skills/recall/SKILL.md +35 -0
  118. package/skills/reflect/SKILL.md +65 -0
  119. package/skills/reflect/references/divergent-reviewer.md +43 -0
  120. package/skills/reflect/references/judgment-reviewer.md +42 -0
  121. package/skills/reflect/references/synthesizer.md +56 -0
  122. package/skills/reflect/references/tooling-reviewer.md +55 -0
  123. package/skills/setup-pstack/SKILL.md +32 -0
  124. package/skills/setup-pstack/references/catalog.md +28 -0
  125. package/skills/setup-pstack/references/spawn.md +61 -0
  126. package/skills/show-me-your-work/SKILL.md +82 -0
  127. package/skills/show-me-your-work/references/decision-log-template.tsv +1 -0
  128. package/skills/show-me-your-work/scripts/log.sh +40 -0
  129. package/skills/swarm/SKILL.md +44 -0
  130. package/skills/tdd/SKILL.md +44 -0
  131. package/skills/teach/SKILL.md +21 -0
  132. package/skills/technical-writing/SKILL.md +127 -0
  133. package/skills/typescript-best-practices/SKILL.md +30 -0
  134. package/skills/typescript-best-practices/references/patterns.md +313 -0
  135. package/skills/unslop/SKILL.md +67 -0
  136. package/skills/why/SKILL.md +146 -0
  137. package/skills/why/references/epistemics.md +144 -0
  138. package/skills/why/references/investigator-prompt.md +103 -0
  139. package/skills/why/references/source-playbook.md +17 -0
  140. package/skills/why/references/sources/code-archaeology.md +88 -0
  141. package/skills/why/references/sources/databricks.md +70 -0
  142. package/skills/why/references/sources/datadog.md +99 -0
  143. package/skills/why/references/sources/incident-postmortem.md +15 -0
  144. package/skills/why/references/sources/linear.md +48 -0
  145. package/skills/why/references/sources/notion.md +55 -0
  146. package/skills/why/references/sources/sentry.md +100 -0
  147. package/skills/why/references/sources/slack.md +54 -0
  148. package/skills/why/references/synthesizer-prompt.md +135 -0
  149. package/src/catalog-types.ts +54 -0
  150. package/src/catalog.ts +308 -0
  151. package/src/client/PstackSettings.tsx +519 -0
  152. package/src/client/api.ts +56 -0
  153. package/src/client/index.tsx +38 -0
  154. package/src/client/locales.ts +65 -0
  155. package/src/client/poteto-defaults.ts +37 -0
  156. package/src/frontmatter.ts +73 -0
  157. package/src/home.ts +13 -0
  158. package/src/http-trust.ts +38 -0
  159. package/src/ids.ts +39 -0
  160. package/src/index.ts +85 -0
  161. package/src/overlay-model.ts +139 -0
  162. package/src/overlay.ts +40 -0
  163. package/src/persona.ts +38 -0
  164. package/src/plugin-config.ts +29 -0
  165. package/src/request-overlay.ts +40 -0
  166. package/src/roles.ts +55 -0
  167. package/src/settings-api.ts +48 -0
  168. package/src/settings-draft.ts +144 -0
  169. package/src/settings-routes.ts +77 -0
  170. package/src/skills-provider.ts +96 -0
  171. package/src/spawn.ts +52 -0
  172. package/src/tools.ts +226 -0
@@ -0,0 +1,519 @@
1
+ /** Plugin-owned pstack page inside the official Settings shell. */
2
+
3
+ import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
4
+ import { Button } from '@deepseek-ai/dsh-client-ui-primitives'
5
+ import type { LiveRoute } from '../catalog-types.ts'
6
+ import type { Overlay, OverlayRoute } from '../overlay-model.ts'
7
+ import { routeKey } from '../overlay-model.ts'
8
+ import {
9
+ applyInheritAll,
10
+ applyRouteToAll,
11
+ draftToOverlay,
12
+ liveFor,
13
+ overlayToDraft,
14
+ parseRouteSelectValue,
15
+ routeSelectValue,
16
+ selectableRoutes,
17
+ stripIllegalEffort,
18
+ type RoleDraft,
19
+ } from '../settings-draft.ts'
20
+ import { PANEL_ROLES, type PstackRole } from '../roles.ts'
21
+ import { loadSettingsSnapshot, saveSettingsOverlay } from './api.ts'
22
+ import type { PstackSettingsKey } from './locales.ts'
23
+ import { potetoNoteCopy } from './poteto-defaults.ts'
24
+
25
+ const STYLE_ID = 'pstack-dsh-settings-theme'
26
+
27
+ export interface PstackSettingsInjected {
28
+ t: (key: PstackSettingsKey, params?: Record<string, unknown>) => string
29
+ subscribeCatalogChanges: (listener: () => void) => () => void
30
+ }
31
+
32
+ export type PstackSettingsProps = Partial<PstackSettingsInjected>
33
+
34
+ const GROUPS: { id: 'groupPlaybooks' | 'groupSkills' | 'groupVerify' | 'groupPanels'; roles: readonly PstackRole[] }[] = [
35
+ { id: 'groupPlaybooks', roles: ['feature', 'refactoring', 'bug-fix', 'perf-issue', 'hillclimb', 'judgment-and-prose', 'hardest-tasks'] },
36
+ { id: 'groupSkills', roles: ['how-explorer', 'how-explainer', 'why-investigators', 'why-synthesizer', 'reflect-tooling', 'reflect-judgment', 'swarm-workers'] },
37
+ { id: 'groupVerify', roles: ['independent-verifier', 'poteto-agent', 'comment-sicko'] },
38
+ { id: 'groupPanels', roles: PANEL_ROLES.filter(role => role !== 'how-critics') },
39
+ ]
40
+
41
+ const SETTINGS_CSS = `
42
+ .pstack-page { display:flex; flex-direction:column; gap:12px; max-width:720px; color:var(--dsw-alias-label-primary); }
43
+ .pstack-title { margin:0; font-size:16px; line-height:24px; font-weight:500; color:var(--dsw-alias-label-primary); }
44
+ .pstack-intro { margin:0; font-size:14px; line-height:22px; color:var(--dsw-alias-label-tertiary); }
45
+ .pstack-note { margin:0; font-size:12px; line-height:18px; color:var(--dsw-alias-label-tertiary); }
46
+ .pstack-warn { margin:0; font-size:12px; line-height:18px; color:var(--dsw-alias-state-warn-label); }
47
+ .pstack-error { margin:0; font-size:13px; line-height:20px; color:var(--dsw-alias-state-error-primary); }
48
+ .pstack-ok { margin:0; font-size:12px; line-height:18px; color:var(--dsw-alias-state-success-primary); }
49
+ .pstack-toolbar { display:flex; flex-wrap:wrap; gap:8px; align-items:center; }
50
+ .pstack-group { margin:12px 0 0; display:flex; flex-direction:column; gap:8px; }
51
+ .pstack-group-title { margin:0; font-size:12px; line-height:18px; font-weight:500; color:var(--dsw-alias-label-secondary); }
52
+ .pstack-card {
53
+ border:1px solid var(--dsw-alias-border-l2); border-radius:12px;
54
+ padding:12px 14px; display:flex; flex-direction:column; gap:10px;
55
+ background:var(--dsw-alias-bg-module-platform);
56
+ }
57
+ .pstack-card-head { display:flex; align-items:baseline; justify-content:space-between; gap:10px; flex-wrap:wrap; }
58
+ .pstack-role { margin:0; font-size:13px; line-height:20px; font-weight:500; color:var(--dsw-alias-label-primary); font-family:ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; }
59
+ .pstack-fields { display:flex; flex-wrap:wrap; gap:10px; align-items:flex-end; }
60
+ .pstack-field { display:flex; flex-direction:column; gap:4px; min-width:180px; flex:1; }
61
+ .pstack-label { font-size:12px; line-height:18px; color:var(--dsw-alias-label-secondary); }
62
+ .pstack-select {
63
+ height:34px; padding:0 12px; border:1px solid var(--dsw-alias-border-l2); border-radius:8px;
64
+ background:var(--dsw-alias-bg-layer-3); color:var(--dsw-alias-label-primary);
65
+ font:inherit; font-size:13px; line-height:20px;
66
+ }
67
+ .pstack-select:focus-visible { outline:none; border-color:var(--dsw-alias-brand-primary); }
68
+ .pstack-select:disabled { color:var(--dsw-alias-label-tertiary); cursor:default; }
69
+ .pstack-row { display:flex; flex-wrap:wrap; gap:10px; align-items:flex-end; }
70
+ .pstack-path { margin:0; font-size:12px; line-height:18px; color:var(--dsw-alias-label-tertiary); word-break:break-all; }
71
+ `
72
+
73
+ function ensureThemeStyles(): void {
74
+ if (typeof document === 'undefined') return
75
+ if (document.getElementById(STYLE_ID) !== null) return
76
+ const style = document.createElement('style')
77
+ style.id = STYLE_ID
78
+ style.textContent = SETTINGS_CSS
79
+ document.head.appendChild(style)
80
+ }
81
+
82
+ function overlaysEqual(left: Overlay, right: Overlay): boolean {
83
+ return JSON.stringify(left) === JSON.stringify(right)
84
+ }
85
+
86
+ function RoleHead({ role, t }: { role: PstackRole; t: PstackSettingsInjected['t'] }) {
87
+ return (
88
+ <div className="pstack-card-head">
89
+ <p className="pstack-role">{role}</p>
90
+ <p className="pstack-note">{potetoNoteCopy(role, t('potetoPrefix'))}</p>
91
+ </div>
92
+ )
93
+ }
94
+
95
+ function firstSelectable(routes: readonly LiveRoute[]): OverlayRoute | undefined {
96
+ const route = selectableRoutes(routes)[0]
97
+ if (route === undefined) return undefined
98
+ return { provider: route.provider, model: route.model }
99
+ }
100
+
101
+ function EffortSelect({
102
+ id,
103
+ route,
104
+ live,
105
+ t,
106
+ onChange,
107
+ }: {
108
+ id: string
109
+ route: OverlayRoute
110
+ live: readonly LiveRoute[]
111
+ t: PstackSettingsInjected['t']
112
+ onChange: (next: OverlayRoute) => void
113
+ }) {
114
+ const found = liveFor(live, route.provider, route.model)
115
+ if (found === undefined || found.efforts.length === 0) return null
116
+ return (
117
+ <div className="pstack-field">
118
+ <label className="pstack-label" htmlFor={id}>{t('effort')}</label>
119
+ <select
120
+ id={id}
121
+ className="pstack-select"
122
+ value={route.reasoningEffort ?? ''}
123
+ onChange={event => {
124
+ const idValue = event.target.value
125
+ onChange(idValue.length === 0
126
+ ? { provider: route.provider, model: route.model }
127
+ : { provider: route.provider, model: route.model, reasoningEffort: idValue })
128
+ }}
129
+ >
130
+ <option value="">{t('effortOmit')}</option>
131
+ {route.reasoningEffort !== undefined && !found.efforts.some(effort => effort.id === route.reasoningEffort)
132
+ ? <option value={route.reasoningEffort} disabled>{route.reasoningEffort} · {t('unavailable')}</option>
133
+ : null}
134
+ {found.efforts.map(effort => (
135
+ <option key={effort.id} value={effort.id}>{effort.name || effort.id}</option>
136
+ ))}
137
+ </select>
138
+ </div>
139
+ )
140
+ }
141
+
142
+ function RouteSelect({
143
+ id,
144
+ value,
145
+ live,
146
+ t,
147
+ disabled,
148
+ onChange,
149
+ }: {
150
+ id: string
151
+ value: string
152
+ live: readonly LiveRoute[]
153
+ t: PstackSettingsInjected['t']
154
+ disabled: boolean
155
+ onChange: (value: string) => void
156
+ }) {
157
+ const selectable = selectableRoutes(live)
158
+ return (
159
+ <select id={id} className="pstack-select" value={value} disabled={disabled} onChange={event => { onChange(event.target.value) }}>
160
+ <option value="inherit-parent">{t('inheritParent')}</option>
161
+ <option value="auto">{t('auto')}</option>
162
+ {value !== 'inherit-parent' && value !== 'auto' && !selectable.some(route => routeKey(route.provider, route.model) === value)
163
+ ? <option value={value} disabled>{value.replace('::', '/')} · {t('unavailable')}</option>
164
+ : null}
165
+ {selectable.map(route => {
166
+ const key = routeKey(route.provider, route.model)
167
+ return (
168
+ <option key={key} value={key}>
169
+ {route.provider}/{route.model}
170
+ </option>
171
+ )
172
+ })}
173
+ </select>
174
+ )
175
+ }
176
+
177
+ export function PstackSettings({ t, subscribeCatalogChanges }: PstackSettingsProps) {
178
+ if (t === undefined) throw new Error('pstack settings requires its translation function')
179
+ const [drafts, setDrafts] = useState<RoleDraft[] | undefined>(undefined)
180
+ const [saved, setSaved] = useState<Overlay | undefined>(undefined)
181
+ const [live, setLive] = useState<readonly LiveRoute[]>([])
182
+ const [path, setPath] = useState<string>('')
183
+ const [recommendOauth, setRecommendOauth] = useState(false)
184
+ const [selectableCount, setSelectableCount] = useState(0)
185
+ const [dropped, setDropped] = useState<string[]>([])
186
+ const [error, setError] = useState<string | undefined>(undefined)
187
+ const [catalogError, setCatalogError] = useState<string | undefined>(undefined)
188
+ const [notice, setNotice] = useState<string | undefined>(undefined)
189
+ const [busy, setBusy] = useState(false)
190
+ const initialized = useRef(false)
191
+ const catalogRequest = useRef<AbortController | undefined>(undefined)
192
+
193
+ useEffect(() => { ensureThemeStyles() }, [])
194
+
195
+ const refresh = useCallback(async () => {
196
+ catalogRequest.current?.abort()
197
+ const request = new AbortController()
198
+ catalogRequest.current = request
199
+ try {
200
+ const snapshot = await loadSettingsSnapshot(request.signal)
201
+ if (request.signal.aborted) return
202
+ setLive(snapshot.catalog.routes)
203
+ setSelectableCount(snapshot.catalog.selectableCount)
204
+ setRecommendOauth(snapshot.catalog.recommendOauthLogin)
205
+ setPath(snapshot.path)
206
+ // Login/model notifications must never replace the user's in-progress
207
+ // role choices (or the baseline used to decide whether Save is enabled).
208
+ if (!initialized.current) {
209
+ setSaved(snapshot.overlay)
210
+ setDrafts(overlayToDraft(snapshot.overlay))
211
+ setDropped(snapshot.droppedRoles)
212
+ initialized.current = true
213
+ }
214
+ setCatalogError(undefined)
215
+ } catch (caught: unknown) {
216
+ if (!request.signal.aborted) {
217
+ setCatalogError(caught instanceof Error ? caught.message : t('requestFailed'))
218
+ }
219
+ }
220
+ }, [t])
221
+
222
+ useEffect(() => {
223
+ const reload = () => { void refresh() }
224
+ const visible = () => { if (document.visibilityState === 'visible') reload() }
225
+ const stop = subscribeCatalogChanges?.(reload)
226
+ window.addEventListener('focus', reload)
227
+ document.addEventListener('visibilitychange', visible)
228
+ reload()
229
+ return () => {
230
+ stop?.()
231
+ window.removeEventListener('focus', reload)
232
+ document.removeEventListener('visibilitychange', visible)
233
+ catalogRequest.current?.abort()
234
+ }
235
+ }, [refresh, subscribeCatalogChanges])
236
+
237
+ const currentOverlay = useMemo(() => drafts === undefined ? undefined : draftToOverlay(drafts), [drafts])
238
+ const dirty = saved !== undefined && currentOverlay !== undefined && !overlaysEqual(saved, currentOverlay)
239
+ const empty = selectableCount === 0
240
+ const applyTargets = selectableRoutes(live)
241
+
242
+ const patchRole = (role: PstackRole, update: (draft: RoleDraft) => RoleDraft): void => {
243
+ setDrafts(current => current?.map(draft => draft.role === role ? update(draft) : draft))
244
+ setNotice(undefined)
245
+ }
246
+
247
+ const setScalarChoice = (role: PstackRole, value: string): void => {
248
+ const parsed = parseRouteSelectValue(value, live)
249
+ patchRole(role, draft => {
250
+ if (parsed.inherit) {
251
+ return { ...draft, inherit: true, inheritChoice: parsed.inheritChoice, routes: [] }
252
+ }
253
+ return { ...draft, inherit: false, inheritChoice: 'inherit-parent', routes: [stripIllegalEffort(parsed.route, live)] }
254
+ })
255
+ }
256
+
257
+ const setPanelChoice = (role: PstackRole, index: number, value: string): void => {
258
+ const parsed = parseRouteSelectValue(value, live)
259
+ patchRole(role, draft => {
260
+ if (parsed.inherit) {
261
+ return { ...draft, inherit: true, inheritChoice: parsed.inheritChoice, routes: [] }
262
+ }
263
+ const routes = draft.inherit ? [] : [...draft.routes]
264
+ routes[index] = stripIllegalEffort(parsed.route, live)
265
+ return { ...draft, inherit: false, inheritChoice: 'inherit-parent', routes }
266
+ })
267
+ }
268
+
269
+ const save = async (): Promise<void> => {
270
+ if (currentOverlay === undefined) return
271
+ setBusy(true)
272
+ setNotice(undefined)
273
+ setError(undefined)
274
+ try {
275
+ const result = await saveSettingsOverlay(currentOverlay)
276
+ setSaved(result.overlay)
277
+ setDrafts(overlayToDraft(result.overlay))
278
+ setDropped([])
279
+ setError(undefined)
280
+ setNotice(t('saved'))
281
+ } catch (caught: unknown) {
282
+ setNotice(undefined)
283
+ setError(caught instanceof Error ? caught.message : t('requestFailed'))
284
+ } finally {
285
+ setBusy(false)
286
+ }
287
+ }
288
+
289
+ return (
290
+ <section
291
+ className="pstack-page"
292
+ aria-labelledby="pstack-settings-title"
293
+ onFocusCapture={event => { if (event.target instanceof HTMLSelectElement) void refresh() }}
294
+ onPointerDownCapture={event => { if (event.target instanceof HTMLSelectElement) void refresh() }}
295
+ >
296
+ <h2 id="pstack-settings-title" className="pstack-title">{t('title')}</h2>
297
+ <p className="pstack-intro">{t('intro')}</p>
298
+ <p className="pstack-note">{t('catalogUpdates')}</p>
299
+ {error !== undefined ? <p className="pstack-error">{error}</p> : null}
300
+ {catalogError !== undefined ? <p className="pstack-error">{catalogError}</p> : null}
301
+ {notice !== undefined ? <p className="pstack-ok" role="status">{notice}</p> : null}
302
+ {dropped.length > 0 ? <p className="pstack-warn">{t('dropped')}</p> : null}
303
+ {drafts === undefined
304
+ ? <p className="pstack-note">{t('loading')}</p>
305
+ : (
306
+ <>
307
+ {empty ? <p className="pstack-warn">{t('emptyCatalog')}</p> : null}
308
+ {recommendOauth ? <p className="pstack-note">{t('oauthRecommend')}</p> : null}
309
+ <div className="pstack-toolbar">
310
+ <Button variant="primary" size="sm" disabled={busy || !dirty} onClick={() => { void save() }}>
311
+ {busy ? t('saving') : t('save')}
312
+ </Button>
313
+ <Button variant="outline" size="sm" disabled={busy} onClick={() => { void refresh() }}>
314
+ {t('reload')}
315
+ </Button>
316
+ <Button
317
+ variant="ghost"
318
+ size="sm"
319
+ disabled={busy}
320
+ onClick={() => {
321
+ setDrafts(current => current === undefined ? current : applyInheritAll(current))
322
+ setNotice(undefined)
323
+ }}
324
+ >
325
+ {t('resetAll')}
326
+ </Button>
327
+ {applyTargets.length > 0
328
+ ? (
329
+ <label className="pstack-field" style={{ minWidth: 220 }}>
330
+ <span className="pstack-label">{t('applyAll')}</span>
331
+ <select
332
+ className="pstack-select"
333
+ defaultValue=""
334
+ disabled={busy}
335
+ onChange={event => {
336
+ const value = event.target.value
337
+ event.target.value = ''
338
+ if (value.length === 0) return
339
+ if (value === 'inherit-parent') {
340
+ setDrafts(current => current === undefined ? current : applyInheritAll(current))
341
+ return
342
+ }
343
+ const parsed = parseRouteSelectValue(value, live)
344
+ if (parsed.inherit) {
345
+ setDrafts(current => current === undefined ? current : applyInheritAll(current, parsed.inheritChoice))
346
+ return
347
+ }
348
+ setDrafts(current => current === undefined ? current : applyRouteToAll(current, parsed.route, live))
349
+ }}
350
+ >
351
+ <option value="">{t('applyAll')}</option>
352
+ <option value="inherit-parent">{t('applyAllInherit')}</option>
353
+ {applyTargets.map(route => {
354
+ const key = routeKey(route.provider, route.model)
355
+ return <option key={key} value={key}>{route.provider}/{route.model}</option>
356
+ })}
357
+ </select>
358
+ </label>
359
+ )
360
+ : null}
361
+ </div>
362
+ {GROUPS.map(group => (
363
+ <div key={group.id} className="pstack-group">
364
+ <h3 className="pstack-group-title">{t(group.id)}</h3>
365
+ {group.roles.map(role => {
366
+ const draft = drafts.find(entry => entry.role === role)
367
+ if (draft === undefined) return null
368
+ if (!draft.panel) {
369
+ const route = draft.routes[0]
370
+ return (
371
+ <article key={role} className="pstack-card">
372
+ <RoleHead role={role} t={t} />
373
+ <div className="pstack-fields">
374
+ <div className="pstack-field">
375
+ <label className="pstack-label" htmlFor={`pstack-role-${role}`}>{role}</label>
376
+ <RouteSelect
377
+ id={`pstack-role-${role}`}
378
+ value={routeSelectValue(draft)}
379
+ live={live}
380
+ t={t}
381
+ disabled={busy}
382
+ onChange={value => { setScalarChoice(role, value) }}
383
+ />
384
+ </div>
385
+ {route !== undefined && !draft.inherit
386
+ ? (
387
+ <EffortSelect
388
+ id={`pstack-effort-${role}`}
389
+ route={route}
390
+ live={live}
391
+ t={t}
392
+ onChange={next => {
393
+ patchRole(role, current => ({ ...current, routes: [next] }))
394
+ }}
395
+ />
396
+ )
397
+ : null}
398
+ </div>
399
+ </article>
400
+ )
401
+ }
402
+ const rows = draft.inherit ? [] : draft.routes
403
+ return (
404
+ <article key={role} className="pstack-card">
405
+ <RoleHead role={role} t={t} />
406
+ {draft.inherit
407
+ ? (
408
+ <div className="pstack-field">
409
+ <label className="pstack-label" htmlFor={`pstack-role-${role}`}>{t('inheritParent')}</label>
410
+ <RouteSelect
411
+ id={`pstack-role-${role}`}
412
+ value={draft.inheritChoice}
413
+ live={[]}
414
+ t={t}
415
+ disabled={busy}
416
+ onChange={value => {
417
+ if (value === 'inherit-parent' || value === 'auto') {
418
+ patchRole(role, current => ({
419
+ ...current,
420
+ inherit: true,
421
+ inheritChoice: value,
422
+ routes: [],
423
+ }))
424
+ }
425
+ }}
426
+ />
427
+ </div>
428
+ )
429
+ : (
430
+ <Button
431
+ variant="ghost"
432
+ size="sm"
433
+ disabled={busy}
434
+ onClick={() => {
435
+ patchRole(role, current => ({
436
+ ...current,
437
+ inherit: true,
438
+ inheritChoice: 'inherit-parent',
439
+ routes: [],
440
+ }))
441
+ }}
442
+ >
443
+ {t('inheritParent')}
444
+ </Button>
445
+ )}
446
+ {!draft.inherit
447
+ ? rows.map((route, index) => (
448
+ <div key={`${role}-${index}`} className="pstack-row">
449
+ <div className="pstack-field">
450
+ <label className="pstack-label" htmlFor={`pstack-panel-${role}-${index}`}>{`${role}[${index}]`}</label>
451
+ <RouteSelect
452
+ id={`pstack-panel-${role}-${index}`}
453
+ value={routeSelectValue({ ...draft, inherit: false, routes: [route] })}
454
+ live={live}
455
+ t={t}
456
+ disabled={busy || empty}
457
+ onChange={value => { setPanelChoice(role, index, value) }}
458
+ />
459
+ </div>
460
+ <EffortSelect
461
+ id={`pstack-panel-effort-${role}-${index}`}
462
+ route={route}
463
+ live={live}
464
+ t={t}
465
+ onChange={next => {
466
+ patchRole(role, current => {
467
+ const nextRoutes = [...current.routes]
468
+ nextRoutes[index] = next
469
+ return { ...current, inherit: false, routes: nextRoutes }
470
+ })
471
+ }}
472
+ />
473
+ <Button
474
+ variant="ghost"
475
+ size="sm"
476
+ disabled={busy}
477
+ onClick={() => {
478
+ patchRole(role, current => {
479
+ const nextRoutes = current.routes.filter((_, item) => item !== index)
480
+ return nextRoutes.length === 0
481
+ ? { ...current, inherit: true, routes: [] }
482
+ : { ...current, inherit: false, routes: nextRoutes }
483
+ })
484
+ }}
485
+ >
486
+ {t('removeRoute')}
487
+ </Button>
488
+ </div>
489
+ ))
490
+ : null}
491
+ <div>
492
+ <Button
493
+ variant="outline"
494
+ size="sm"
495
+ disabled={busy || empty}
496
+ onClick={() => {
497
+ const added = firstSelectable(live)
498
+ if (added === undefined) return
499
+ patchRole(role, current => ({
500
+ ...current,
501
+ inherit: false,
502
+ routes: [...(current.inherit ? [] : current.routes), added],
503
+ }))
504
+ }}
505
+ >
506
+ {t('addRoute')}
507
+ </Button>
508
+ </div>
509
+ </article>
510
+ )
511
+ })}
512
+ </div>
513
+ ))}
514
+ {path.length > 0 ? <p className="pstack-path">{t('overlayPath')}: {path}</p> : null}
515
+ </>
516
+ )}
517
+ </section>
518
+ )
519
+ }
@@ -0,0 +1,56 @@
1
+ import type { CatalogResult } from '../catalog-types.ts'
2
+ import type { Overlay } from '../overlay-model.ts'
3
+ import { SETTINGS_SAVE_PATH, SETTINGS_SNAPSHOT_PATH } from '../ids.ts'
4
+
5
+ export interface SettingsSnapshot {
6
+ catalog: CatalogResult
7
+ overlay: Overlay
8
+ path: string
9
+ missing: boolean
10
+ droppedRoles: string[]
11
+ }
12
+
13
+ type CatalogEvent = 'llm/adapters-updated' | 'settings/document-updated' | 'credentials/reference-updated'
14
+
15
+ export interface CatalogEventContext {
16
+ remote: { $on(event: CatalogEvent, listener: () => void): () => void }
17
+ on(event: 'connection/reset', listener: () => void): () => void
18
+ }
19
+
20
+ /** Use the same Host notifications as DSH's model directory, scoped to the open settings page. */
21
+ export function listenForCatalogChanges(ctx: CatalogEventContext, listener: () => void): () => void {
22
+ const disposers = [
23
+ ctx.remote.$on('llm/adapters-updated', listener),
24
+ ctx.remote.$on('settings/document-updated', listener),
25
+ ctx.remote.$on('credentials/reference-updated', listener),
26
+ ctx.on('connection/reset', listener),
27
+ ]
28
+ return () => { for (const dispose of disposers) dispose() }
29
+ }
30
+
31
+ async function jsonRequest<T>(path: string, method: string, body?: unknown, signal?: AbortSignal): Promise<T> {
32
+ const response = await fetch(path, {
33
+ method,
34
+ headers: { accept: 'application/json', ...body === undefined ? {} : { 'content-type': 'application/json' } },
35
+ credentials: 'same-origin',
36
+ cache: 'no-store',
37
+ signal,
38
+ ...body === undefined ? {} : { body: JSON.stringify(body) },
39
+ })
40
+ const value: unknown = await response.json().catch(() => undefined)
41
+ if (!response.ok) {
42
+ const message = typeof value === 'object' && value !== null && 'error' in value && typeof (value as { error: unknown }).error === 'string'
43
+ ? (value as { error: string }).error
44
+ : `HTTP ${response.status}`
45
+ throw new Error(message)
46
+ }
47
+ return value as T
48
+ }
49
+
50
+ export function loadSettingsSnapshot(signal?: AbortSignal): Promise<SettingsSnapshot> {
51
+ return jsonRequest<SettingsSnapshot>(SETTINGS_SNAPSHOT_PATH, 'GET', undefined, signal)
52
+ }
53
+
54
+ export function saveSettingsOverlay(overlay: Overlay): Promise<{ path: string; overlay: Overlay }> {
55
+ return jsonRequest<{ path: string; overlay: Overlay }>(SETTINGS_SAVE_PATH, 'PUT', { overlay })
56
+ }
@@ -0,0 +1,38 @@
1
+ /** Browser half: pstack roles inside official DSH Settings. */
2
+
3
+ import type { Context } from '@deepseek-ai/cordis'
4
+ import type {} from '@deepseek-ai/dsh-api-remotes/client'
5
+ import type {} from '@deepseek-ai/dsh-client-connection/client'
6
+ import type {} from '@deepseek-ai/dsh-client-ui-settings/client'
7
+ import type {} from '@deepseek-ai/dsh-client-locale/client'
8
+ import type {} from '@deepseek-ai/dsh-client-ui-renderer/client'
9
+ import { PstackSettings } from './PstackSettings.tsx'
10
+ import type { PstackSettingsInjected } from './PstackSettings.tsx'
11
+ import { en, zh } from './locales.ts'
12
+ import type { PstackSettingsKey } from './locales.ts'
13
+ import { SETTINGS_SECTION_ID } from '../ids.ts'
14
+ import { listenForCatalogChanges } from './api.ts'
15
+
16
+ declare module '@deepseek-ai/dsh-client-ui-slots' {
17
+ interface LocaleNamespaceMap {
18
+ 'settings.pstack': PstackSettingsKey
19
+ }
20
+ }
21
+
22
+ export const name = 'pstack-dsh-client'
23
+ export const inject = ['slots', 'locale', 'remote']
24
+
25
+ export function apply(ctx: Context): void {
26
+ const namespace = 'settings.pstack'
27
+ ctx.effect(() => ctx.locale.register(namespace, { zh, en }), 'pstack-dsh: settings copy')
28
+ const t: PstackSettingsInjected['t'] = ctx.locale.bind(namespace)
29
+ const subscribeCatalogChanges: PstackSettingsInjected['subscribeCatalogChanges'] = listener =>
30
+ listenForCatalogChanges(ctx, listener)
31
+ ctx.slots.inject('settings.section', () => ctx.slots.register({
32
+ name: 'settings.section',
33
+ id: SETTINGS_SECTION_ID,
34
+ order: 16,
35
+ label: () => t('nav'),
36
+ inject: (): PstackSettingsInjected => ({ t, subscribeCatalogChanges }),
37
+ }, PstackSettings))
38
+ }
@@ -0,0 +1,65 @@
1
+ export const en = {
2
+ nav: 'pstack roles',
3
+ title: 'pstack roles',
4
+ intro: 'Map each pstack role onto a route that is already logged in on this DSH. Children inherit this conversation until you save a mapping. Writes $DSH_HOME/pstack-dsh.json, the same file spawn reads.',
5
+ loading: 'Loading logged-in routes…',
6
+ save: 'Save',
7
+ saving: 'Saving…',
8
+ saved: 'Saved. New pstack_spawn calls pick this up.',
9
+ reload: 'Refresh models',
10
+ catalogUpdates: 'Model choices update automatically after login or model changes. Unsaved role settings are preserved.',
11
+ unavailable: 'currently unavailable',
12
+ resetAll: 'Inherit for every role',
13
+ inheritParent: 'inherit-parent',
14
+ auto: 'auto',
15
+ potetoPrefix: 'Poteto 0.15 recommendation: ',
16
+ emptyCatalog: 'No logged-in route. Every role inherits this conversation. Add a provider API key in Settings → Models.',
17
+ oauthRecommend: 'Subscription logins (ChatGPT, Claude, Grok, Copilot, OpenRouter, Kimi, Google Antigravity) show up here after you install and sign in with dsh-oauth-login or dsh-antigravity-oauth. Not required for API keys.',
18
+ effort: 'Effort',
19
+ effortOmit: 'omit (adapter default)',
20
+ addRoute: 'Add route',
21
+ removeRoute: 'Remove',
22
+ applyAll: 'Apply to every role',
23
+ applyAllInherit: 'Inherit parent for every role',
24
+ overlayPath: 'Overlay',
25
+ dropped: 'Some saved mappings pointed at routes that are no longer logged in. Those roles now inherit until you save again.',
26
+ requestFailed: 'The settings request failed.',
27
+ groupPlaybooks: 'Playbook delegates',
28
+ groupSkills: 'Skill roles',
29
+ groupVerify: 'Verify and helpers',
30
+ groupPanels: 'Panel roles',
31
+ } as const
32
+
33
+ export type PstackSettingsKey = keyof typeof en
34
+
35
+ export const zh: { [Key in PstackSettingsKey]: string } = {
36
+ nav: 'pstack 角色',
37
+ title: 'pstack 角色',
38
+ intro: '把每个 pstack 角色映射到本机已经登录的路由。未保存前,子 agent 继承当前对话。写入 $DSH_HOME/pstack-dsh.json,与 spawn 读的是同一份文件。',
39
+ loading: '正在加载已登录路由…',
40
+ save: '保存',
41
+ saving: '保存中…',
42
+ saved: '已保存。之后的 pstack_spawn 会读到这份 overlay。',
43
+ reload: '刷新模型列表',
44
+ catalogUpdates: '登录或模型变更后自动更新列表,不会覆盖未保存的角色设置。',
45
+ unavailable: '暂不可用',
46
+ resetAll: '全部继承父对话',
47
+ inheritParent: 'inherit-parent',
48
+ auto: 'auto',
49
+ potetoPrefix: 'Poteto 0.15 推荐:',
50
+ emptyCatalog: '没有已登录路由。每个角色都继承当前对话。请先在 设置 → 模型 里配置 API key。',
51
+ oauthRecommend: '订阅登录(ChatGPT / Claude / Grok / Copilot / OpenRouter / Kimi / Google Antigravity)要出现在这里,需要另装并登录 dsh-oauth-login 或 dsh-antigravity-oauth。只用 API key 可以不装。',
52
+ effort: 'Effort',
53
+ effortOmit: '省略(适配器默认)',
54
+ addRoute: '添加路由',
55
+ removeRoute: '移除',
56
+ applyAll: '应用到全部角色',
57
+ applyAllInherit: '全部角色继承父对话',
58
+ overlayPath: 'Overlay',
59
+ dropped: '有些已保存的映射指向了不再登录的路由。这些角色已改回继承,保存后才会写盘。',
60
+ requestFailed: '设置请求失败。',
61
+ groupPlaybooks: '玩法委派',
62
+ groupSkills: '技能角色',
63
+ groupVerify: '核验与助手',
64
+ groupPanels: '面板角色',
65
+ }