Skip to content

Commit d3b7f09

Browse files
authored
Merge pull request #19 from deepagent-ltd/dev
V1.1
2 parents 02a104c + 4c5d65f commit d3b7f09

133 files changed

Lines changed: 14042 additions & 350 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

‎.gitignore‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,3 +36,6 @@ tsconfig.tsbuildinfo
3636
# Internal design docs — kept local-only, not tracked in git (see S2-v3.4.1 DOC-2).
3737
# Erased from history via git-filter-repo; the public distilled version lives in design/.
3838
docs/
39+
40+
# Non-code domain packs — own git repo (deepagent-domain), not tracked by main repo.
41+
extra-domain/

‎packages/app/package.json‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
"exports": {
77
".": "./src/index.ts",
88
"./desktop-menu": "./src/desktop-menu.ts",
9+
"./zoom-levels": "./src/zoom-levels.ts",
910
"./updater": "./src/updater.ts",
1011
"./wsl/types": "./src/wsl/types.ts",
1112
"./vite": "./vite.js",

‎packages/app/src/components/dialog-connect-provider.tsx‎

Lines changed: 142 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import type { ProviderAuthAuthorization, ProviderAuthMethod } from "@deepagent-code/sdk/v2/client"
22
import { Button } from "@deepagent-code/ui/button"
3+
import { Collapsible } from "@deepagent-code/ui/collapsible"
34
import { useDialog } from "@deepagent-code/ui/context/dialog"
45
import { Dialog } from "@deepagent-code/ui/dialog"
56
import { Icon } from "@deepagent-code/ui/icon"
@@ -16,19 +17,30 @@ import { useServerSDK } from "@/context/server-sdk"
1617
import { useServerSync } from "@/context/server-sync"
1718
import { useLanguage } from "@/context/language"
1819
import { useProviders } from "@/hooks/use-providers"
20+
import { isOfficialProvider } from "@deepagent-code/core/provider-official"
1921

2022
// Recommended/official providers ship a fixed protocol (npm) tied to a canonical endpoint. Letting
2123
// users point one of these at a relay/local server silently breaks because the protocol no longer
2224
// matches the endpoint — those users must add a custom provider instead. So the connect dialog hides
23-
// the baseURL field for these and only exposes it for ad-hoc config/custom providers.
25+
// the baseURL field for these and only exposes it for ad-hoc config/custom providers. Google and xAI
26+
// resolve their real endpoint from the bundled SDK; the URL here only satisfies form validation and
27+
// the placeholder. Keys must exist for every official id (see OFFICIAL_PROVIDER_IDS in core).
2428
const providerBaseURL: Record<string, string> = {
2529
openai: "https://api.openai.com/v1",
2630
deepseek: "https://api.deepseek.com",
2731
anthropic: "https://api.anthropic.com/v1",
2832
zhipuai: "https://open.bigmodel.cn/api/paas/v4",
33+
"zhipuai-coding-plan": "https://open.bigmodel.cn/api/coding/paas/v4",
34+
zai: "https://api.z.ai/api/paas/v4",
35+
"zai-coding-plan": "https://api.z.ai/api/coding/paas/v4",
36+
xai: "https://api.x.ai/v1",
37+
google: "https://generativelanguage.googleapis.com/v1beta",
2938
}
3039

31-
const isBuiltinProvider = (providerID: string) => providerID in providerBaseURL
40+
// Official = first-party in the backend (key store credentials, fixed protocol). This must match the
41+
// backend's OFFICIAL_PROVIDER_IDS exactly so the connect flow routes credentials the same way the
42+
// loader reads them (auth for official, config options.apiKey for third-party).
43+
const isBuiltinProvider = (providerID: string) => isOfficialProvider(providerID)
3244

3345
const providerKind = (providerID: string) => (providerID === "anthropic" ? "anthropic" : "openai-compatible")
3446

@@ -419,6 +431,13 @@ export function DialogConnectProvider(props: { provider: string }) {
419431
id: model.id,
420432
name: model.name || model.id,
421433
}))
434+
// Official-provider transport settings are surfaced by the config overlay under
435+
// provider.<id>.options (backed by SettingsStore, not the config file). Seed the fields from there.
436+
const transportOption = (key: string): string => {
437+
const value = serverSync.data.config.provider?.[props.provider]?.options?.[key]
438+
if (value === false) return "0"
439+
return typeof value === "number" ? String(value) : ""
440+
}
422441
const [formStore, setFormStore] = createStore({
423442
value: "",
424443
baseURL:
@@ -430,8 +449,42 @@ export function DialogConnectProvider(props: { provider: string }) {
430449
discovered: [] as DiscoveredProviderModel[],
431450
selectedModel: undefined as string | undefined,
432451
discovering: false,
452+
headerTimeout: transportOption("headerTimeout"),
453+
chunkTimeout: transportOption("chunkTimeout"),
454+
timeout: transportOption("timeout"),
455+
maxRetries: transportOption("maxRetries"),
456+
transportError: undefined as string | undefined,
433457
})
434458

459+
// Parse a transport field: empty → undefined (use default); "0" for header/request timeout →
460+
// false (disabled); otherwise a positive integer. Returns { ok, value } so callers can reject
461+
// malformed input.
462+
function parseTransport(raw: string, allowFalse: boolean): { ok: boolean; value: number | false | undefined } {
463+
const trimmed = raw.trim()
464+
if (trimmed === "") return { ok: true, value: undefined }
465+
const n = Number(trimmed)
466+
if (!Number.isInteger(n) || n < 0) return { ok: false, value: undefined }
467+
if (n === 0) return allowFalse ? { ok: true, value: false } : { ok: false, value: undefined }
468+
return { ok: true, value: n }
469+
}
470+
471+
// Build the SettingsStore-bound options patch for an official provider (routed via the config
472+
// overlay's intercept). Returns undefined when a field is malformed.
473+
function transportOptions(): Record<string, number | false> | undefined | "invalid" {
474+
const header = parseTransport(formStore.headerTimeout, true)
475+
const chunk = parseTransport(formStore.chunkTimeout, false)
476+
const request = parseTransport(formStore.timeout, true)
477+
const retries = parseTransport(formStore.maxRetries, false)
478+
if (!header.ok || !chunk.ok || !request.ok || !retries.ok) return "invalid"
479+
const options: Record<string, number | false> = {}
480+
if (header.value !== undefined) options.headerTimeout = header.value
481+
if (chunk.value !== undefined && chunk.value !== false) options.chunkTimeout = chunk.value
482+
if (request.value !== undefined) options.timeout = request.value
483+
if (retries.value !== undefined && retries.value !== false) options.maxRetries = retries.value
484+
return options
485+
}
486+
487+
435488
async function handleSubmit(e: SubmitEvent) {
436489
e.preventDefault()
437490

@@ -456,6 +509,43 @@ export function DialogConnectProvider(props: { provider: string }) {
456509

457510
setFormStore("error", undefined)
458511
setFormStore("baseURLError", undefined)
512+
513+
if (isBuiltinProvider(props.provider)) {
514+
const transport = transportOptions()
515+
if (transport === "invalid") {
516+
setFormStore("transportError", language.t("provider.connect.transport.error.number"))
517+
return
518+
}
519+
setFormStore("transportError", undefined)
520+
if (key) {
521+
await serverSDK.client.auth.set({
522+
providerID: props.provider,
523+
auth: {
524+
type: "api",
525+
key,
526+
},
527+
})
528+
}
529+
// Route transport tuning through the config overlay's intercept → SettingsStore. The
530+
// remaining official-provider fields are ignored by the backend loader; only options.<transport>
531+
// is picked up (and stripped from the config file). Always send the key so cleared fields reset.
532+
const disabled = (serverSync.data.config.disabled_providers ?? []).filter((id) => id !== props.provider)
533+
await serverSync.updateConfig({
534+
disabled_providers: disabled,
535+
provider: {
536+
[props.provider]: { options: transport },
537+
},
538+
})
539+
await serverSDK.client.global.dispose()
540+
showToast({
541+
variant: "success",
542+
icon: "circle-check",
543+
title: language.t("provider.connect.toast.connected.title", { provider: provider().name }),
544+
description: language.t("provider.connect.toast.connected.description", { provider: provider().name }),
545+
})
546+
return
547+
}
548+
459549
setFormStore("discovering", true)
460550
try {
461551
const discovered = await serverSDK.client.provider.models
@@ -484,15 +574,6 @@ export function DialogConnectProvider(props: { provider: string }) {
484574
setFormStore("discovered", nextModels)
485575
setFormStore("selectedModel", nextSelected.id)
486576

487-
if (key) {
488-
await serverSDK.client.auth.set({
489-
providerID: props.provider,
490-
auth: {
491-
type: "api",
492-
key,
493-
},
494-
})
495-
}
496577
const current = serverSync.data.config.provider?.[props.provider] ?? {}
497578
await serverSync.updateConfig({
498579
provider: {
@@ -502,6 +583,7 @@ export function DialogConnectProvider(props: { provider: string }) {
502583
options: {
503584
...(current.options ?? {}),
504585
baseURL,
586+
...(key ? { apiKey: key } : {}),
505587
},
506588
models: {
507589
...(current.models ?? {}),
@@ -558,6 +640,55 @@ export function DialogConnectProvider(props: { provider: string }) {
558640
validationState={formStore.error ? "invalid" : undefined}
559641
error={formStore.error}
560642
/>
643+
<Show when={isBuiltinProvider(props.provider)}>
644+
<Collapsible class="w-full">
645+
<Collapsible.Trigger class="flex w-full items-center justify-between gap-2 text-13-medium text-text-base">
646+
<span>{language.t("provider.connect.transport.title")}</span>
647+
<Collapsible.Arrow />
648+
</Collapsible.Trigger>
649+
<Collapsible.Content>
650+
<div class="flex flex-col gap-4 pt-4">
651+
<div class="text-12-regular text-text-weak">
652+
{language.t("provider.connect.transport.description")}
653+
</div>
654+
<TextField
655+
type="text"
656+
inputmode="numeric"
657+
label={language.t("provider.connect.transport.headerTimeout.label")}
658+
placeholder={language.t("provider.connect.transport.headerTimeout.placeholder")}
659+
value={formStore.headerTimeout}
660+
onChange={(v) => setFormStore("headerTimeout", v)}
661+
/>
662+
<TextField
663+
type="text"
664+
inputmode="numeric"
665+
label={language.t("provider.connect.transport.chunkTimeout.label")}
666+
placeholder={language.t("provider.connect.transport.chunkTimeout.placeholder")}
667+
value={formStore.chunkTimeout}
668+
onChange={(v) => setFormStore("chunkTimeout", v)}
669+
/>
670+
<TextField
671+
type="text"
672+
inputmode="numeric"
673+
label={language.t("provider.connect.transport.timeout.label")}
674+
placeholder={language.t("provider.connect.transport.timeout.placeholder")}
675+
value={formStore.timeout}
676+
onChange={(v) => setFormStore("timeout", v)}
677+
/>
678+
<TextField
679+
type="text"
680+
inputmode="numeric"
681+
label={language.t("provider.connect.transport.maxRetries.label")}
682+
placeholder={language.t("provider.connect.transport.maxRetries.placeholder")}
683+
value={formStore.maxRetries}
684+
onChange={(v) => setFormStore("maxRetries", v)}
685+
validationState={formStore.transportError ? "invalid" : undefined}
686+
error={formStore.transportError}
687+
/>
688+
</div>
689+
</Collapsible.Content>
690+
</Collapsible>
691+
</Show>
561692
<Button class="w-auto" type="submit" size="large" variant="primary" disabled={formStore.discovering}>
562693
{language.t("common.continue")}
563694
</Button>

‎packages/app/src/components/dialog-custom-provider-form.ts‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -142,6 +142,7 @@ export function validateCustomProvider(input: ValidateArgs) {
142142
...(env ? { env: [env] } : {}),
143143
options: {
144144
baseURL,
145+
...(key ? { apiKey: key } : {}),
145146
...(Object.keys(headerConfig).length ? { headers: headerConfig } : {}),
146147
},
147148
models: modelConfig,

‎packages/app/src/components/dialog-custom-provider.tsx‎

Lines changed: 0 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -132,16 +132,6 @@ export function DialogCustomProvider(props: Props) {
132132
const disabledProviders = serverSync.data.config.disabled_providers ?? []
133133
const nextDisabled = disabledProviders.filter((id) => id !== result.providerID)
134134

135-
if (result.key) {
136-
await serverSDK.client.auth.set({
137-
providerID: result.providerID,
138-
auth: {
139-
type: "api",
140-
key: result.key,
141-
},
142-
})
143-
}
144-
145135
await serverSync.updateConfig({
146136
provider: { [result.providerID]: result.config },
147137
disabled_providers: nextDisabled,

‎packages/app/src/components/settings-v2/general.tsx‎

Lines changed: 34 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import { useParams } from "@solidjs/router"
1010
import { useLanguage } from "@/context/language"
1111
import { usePermission } from "@/context/permission"
1212
import { usePlatform, type DisplayBackend } from "@/context/platform"
13+
import { ZOOM_LEVELS } from "@/zoom-levels"
1314
import { useServerSync } from "@/context/server-sync"
1415
import { useServerSDK } from "@/context/server-sdk"
1516
import { useUpdaterAction } from "../updater-action"
@@ -284,6 +285,10 @@ export const SettingsGeneralV2: Component = () => {
284285
})),
285286
)
286287

288+
const zoomOptions = createMemo(() =>
289+
ZOOM_LEVELS.map((level) => ({ value: String(level), label: `${Math.round(level * 100)}%` })),
290+
)
291+
287292
const noneSound = { id: "none", label: "sound.option.none" } as const
288293
const soundOptions = [noneSound, ...SOUND_OPTIONS]
289294
const mono = () => monoInput(settings.appearance.font())
@@ -418,7 +423,18 @@ export const SettingsGeneralV2: Component = () => {
418423
if (option.value === deepAgentWishModel()) return
419424
void updateDeepAgentOptions(serverSync, { wishModel: option.value })
420425
}}
421-
/>
426+
>
427+
{(o) => (
428+
<span class="flex items-center gap-2">
429+
<span>{o.label}</span>
430+
<Show when={o.description}>
431+
<span class="rounded-sm bg-background-strong px-1.5 py-0.5 text-11-regular text-text-weak">
432+
{o.description}
433+
</span>
434+
</Show>
435+
</span>
436+
)}
437+
</SelectV2>
422438
</SettingsRowV2>
423439

424440
<SettingsRowV2
@@ -811,6 +827,23 @@ export const SettingsGeneralV2: Component = () => {
811827
</div>
812828
</SettingsRowV2>
813829

830+
<SettingsRowV2
831+
title={language.t("settings.general.row.zoom.title")}
832+
description={language.t("settings.general.row.zoom.description")}
833+
>
834+
<SelectV2
835+
appearance="inline"
836+
data-action="settings-zoom"
837+
options={zoomOptions()}
838+
current={zoomOptions().find((o) => o.value === String(platform.webviewZoom?.()))}
839+
placement="bottom-end"
840+
gutter={6}
841+
value={(o) => o.value}
842+
label={(o) => o.label}
843+
onSelect={(option) => option && platform.setWebviewZoom?.(Number(option.value))}
844+
/>
845+
</SettingsRowV2>
846+
814847
<Show when={linux()}>
815848
<SettingsRowV2
816849
title={

‎packages/app/src/components/windows-app-menu.tsx‎

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,9 @@ import { IconButtonV2 } from "@deepagent-code/ui/v2/icon-button-v2"
66
import { Icon as IconV2 } from "@deepagent-code/ui/v2/icon"
77

88
import { useCommand } from "@/context/command"
9-
import { DESKTOP_MENU, desktopMenuVisible, type DesktopMenuAction, type DesktopMenuEntry } from "@/desktop-menu"
9+
import { DESKTOP_MENU, desktopMenuVisible, type DesktopMenuAction, type DesktopMenuEntry, type DesktopMenuItem } from "@/desktop-menu"
1010
import { usePlatform } from "@/context/platform"
11+
import { canZoomIn, canZoomOut, zoomPercent } from "@/zoom-levels"
1112

1213
export function WindowsAppMenu(props: {
1314
command: ReturnType<typeof useCommand>
@@ -46,6 +47,18 @@ export function WindowsAppMenu(props: {
4647
if (entry.href) props.platform.openLink(entry.href)
4748
}
4849

50+
const zoom = () => props.platform.webviewZoom?.() ?? 1
51+
const isZoomAction = (action: DesktopMenuAction | undefined) =>
52+
action === "view.zoomIn" || action === "view.zoomOut" || action === "view.resetZoom"
53+
const zoomLabel = (entry: DesktopMenuItem) =>
54+
isZoomAction(entry.action) ? `${entry.label ?? ""} ${zoomPercent(zoom())}%` : entry.label ?? ""
55+
const zoomDisabled = (entry: DesktopMenuItem) => {
56+
if (entry.command) return commandDisabled(entry.command)
57+
if (entry.action === "view.zoomIn") return !canZoomIn(zoom())
58+
if (entry.action === "view.zoomOut") return !canZoomOut(zoom())
59+
return false
60+
}
61+
4962
return (
5063
<DropdownMenu gutter={4} modal={false} placement="bottom-start">
5164
{props.variant === "v2" ? (
@@ -87,9 +100,9 @@ export function WindowsAppMenu(props: {
87100
<DropdownMenu.Separator />
88101
) : (
89102
<DesktopMenuItem
90-
label={entry.label ?? ""}
103+
label={zoomLabel(entry)}
91104
keybind={entry.command ? props.command.keybind(entry.command) : entry.accelerator?.windows}
92-
disabled={entry.command ? commandDisabled(entry.command) : false}
105+
disabled={zoomDisabled(entry)}
93106
onSelect={() => runEntry(entry)}
94107
/>
95108
),

‎packages/app/src/context/global-sync/types.ts‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,9 +35,10 @@ export type ProjectMeta = {
3535
export type SessionPlanStep = {
3636
step_id: string
3737
title: string
38-
status: string // pending | active | done | cancelled
38+
status: string // pending | active | done | cancelled | blocked
3939
acceptance?: string | null
4040
assigned_agent?: string | null
41+
note?: string | null // U10: blocker explanation when status is "blocked"
4142
}
4243
export type SessionPlan = {
4344
plan_id: string

0 commit comments

Comments
 (0)