Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
121 commits
Select commit Hold shift + click to select a range
17b5c73
feat(app): tasks panel for background subagents and shell tools
gmhschn Sep 4, 2026
27a10f3
feat(app): tasks model fallback to task tool metadata
gmhschn Sep 4, 2026
d6b6b6e
feat(app): tasks ticker only while live work present
gmhschn Sep 4, 2026
519702e
feat(app): tasks i18n polish, locale-aware numbers
gmhschn Sep 4, 2026
77d1e63
feat(app): english fallback for new tasks keys across locales
gmhschn Sep 4, 2026
e58be25
feat(app): locale-free headlines in tasks data hook
gmhschn Sep 4, 2026
23c1207
Merge pull request #1 from gmhelmold/feat/tasks-pane
gmhelmold Sep 4, 2026
4953c44
feat(opencode): task tool accepts explicit provider/model per subagent
gmhschn Sep 4, 2026
1e031f5
feat(opencode): model-scoped task permissions for subagent allowlist
gmhschn Sep 4, 2026
ccdfce1
fix(opencode): readonly patterns type in task model-scope test
gmhschn Sep 4, 2026
3abcc37
feat(app): subagent model allowlist picker dialog
gmhschn Sep 4, 2026
abae400
feat(app): subagent-models control in composer toolbar
gmhschn Sep 4, 2026
19b73aa
feat(hooks): add session.start, session.end, stop lifecycle hooks (#2)
gmhelmold Sep 4, 2026
886ea11
feat(app): align subagent-models switch with manage dialog pattern
gmhschn Sep 4, 2026
a57a1ed
feat(app): subagent-models control in legacy composer toolbar
gmhschn Sep 5, 2026
bd8b446
feat(providers): multiple labeled API keys per provider (#3)
gmhelmold Sep 5, 2026
9b573f8
feat(app): pre-first-message subagent model scoping
gmhschn Sep 5, 2026
55d725c
feat(opencode): session subagent-model allowlist in task tool descrip…
gmhschn Sep 5, 2026
fc6b12b
fix(app): tasks dedup by child session and model read from promise shape
gmhschn Sep 5, 2026
649c01c
Merge pull request #4 from gmhelmold/feat/tasks-pane
gmhelmold Sep 6, 2026
a355c0e
merge-dev
gmhschn Sep 6, 2026
100d916
feat(opencode): add native HuGR composer tools (#8)
gmhelmold Sep 7, 2026
46ce185
perf(core): stop writing duplicate snapshot events in local mode (#7)
gmhelmold Sep 7, 2026
867ccc7
feat(opencode): symbol-scoped reads, callee depth, tail offsets
gmhschn Sep 6, 2026
031df4d
feat(opencode): CLI benchmark harness for the read tool
gmhschn Sep 6, 2026
983b7c6
feat(opencode): read benchmark harness script
gmhschn Sep 6, 2026
c217cae
perf(read): memoized whole-file scan, tail via cache; hardcore benchm…
gmhschn Sep 6, 2026
306d7f9
feat(read): auto-degrade on unresolvable symbol + token-gate PARTIAL …
gmhschn Sep 6, 2026
ec4cd79
docs(read): add final benchmark results (PR #6 @ d1ac7442)
gmhschn Sep 6, 2026
a6a29f6
feat(read): search param auto-resolves symbol range + sparse numberin…
gmhschn Sep 6, 2026
3ecef92
fix(read): preserve symbol line content and benchmark search mode
gmhschn Sep 7, 2026
91e3f37
fix(read): add correctness benchmark harness
gmhschn Sep 7, 2026
07bdd03
fix(read): validate benchmark error outcomes
gmhschn Sep 7, 2026
f9e3ffe
fix(read): benchmark valid oversized range
gmhschn Sep 7, 2026
f249527
fix(opencode): stream read tool output
gmhschn Sep 7, 2026
ad8cdaf
fix(opencode): cap read output exactly
gmhschn Sep 7, 2026
62f3107
fix(opencode): reject empty read partials
gmhschn Sep 7, 2026
1d3cd74
fix(read): allow standalone benchmark self-test
gmhschn Sep 7, 2026
643608a
fix(read): cap media attachment size
gmhschn Sep 7, 2026
1f266b1
test(read): add performance benchmark harness
gmhschn Sep 7, 2026
ccd5a63
test(read): aggregate performance benchmark runs
gmhschn Sep 7, 2026
6dafd63
test(read): persist performance benchmark rows
gmhschn Sep 7, 2026
51093a2
test(read): add model-driven AB evaluator
gmhschn Sep 7, 2026
9e696a9
test(read): persist agent evaluation progress
gmhschn Sep 7, 2026
8b8034c
fix(read): auto-approve agent benchmark corpus
gmhschn Sep 7, 2026
95ec29a
fix(read): capture evaluator child diagnostics
gmhschn Sep 7, 2026
228475e
fix(read): use absolute agent corpus paths
gmhschn Sep 7, 2026
11d73f8
test(read): select agent evaluation tasks
gmhschn Sep 7, 2026
e79475d
fix(read): accept numeric string offsets
gmhschn Sep 7, 2026
b0c2bb9
fix(read): type numeric parameter coercion
gmhschn Sep 7, 2026
a4c1db2
fix(compaction): skip missing provider autocontinue
gmhschn Sep 7, 2026
658868f
fix(cli): type lazy command arguments
gmhschn Sep 7, 2026
55e346d
fix(opencode): harden composer and workspace boundaries (#9)
gmhelmold Sep 8, 2026
46ac922
fix(opencode): abort warp on patch failure (#10)
gmhelmold Sep 8, 2026
f2b2f33
feat(desktop): add app dock browser
gmhschn Sep 9, 2026
09f9b36
fix(desktop): harden app dock views
gmhschn Sep 9, 2026
ec148f1
fix(desktop): define app dock event contract
gmhschn Sep 9, 2026
f0e7612
fix(desktop): bridge app dock events
gmhschn Sep 9, 2026
ba57ee7
fix(desktop): project app dock IPC events
gmhschn Sep 9, 2026
81bc24b
test(desktop): add app dock security harness
gmhschn Sep 9, 2026
cf8ff0c
fix(desktop): route app dock harness through IPC
gmhschn Sep 9, 2026
be26dc9
test(desktop): diagnose app dock harness startup
gmhschn Sep 9, 2026
9ef8cb3
test(desktop): use static app dock electron entry
gmhschn Sep 9, 2026
ac37d96
fix(desktop): emit runnable app dock harness bundle
gmhschn Sep 9, 2026
95d59b0
fix(desktop): use node fixture file reads
gmhschn Sep 9, 2026
c27b7df
fix(desktop): load app dock ipc before readiness
gmhschn Sep 9, 2026
f2899c9
test(desktop): read app dock events from renderer
gmhschn Sep 9, 2026
81d26d0
chore(desktop): ignore app dock artifacts
gmhschn Sep 9, 2026
e3b955f
test(desktop): trace app dock renderer events
gmhschn Sep 9, 2026
d149873
fix(desktop): keep app dock harness alive
gmhschn Sep 9, 2026
e7f6edb
test(desktop): invoke app dock IPC from test frames
gmhschn Sep 9, 2026
1384a22
test(desktop): correlate app dock security events
gmhschn Sep 9, 2026
1ee1b22
fix(desktop): detach App Dock records from owner window
gmhschn Sep 9, 2026
10cb3f9
feat(desktop): close App Dock tab groups
gmhschn Sep 9, 2026
832abb2
test(desktop): close app dock acceptance gaps
gmhschn Sep 9, 2026
9c2101d
test(desktop): harden app dock acceptance
gmhschn Sep 9, 2026
2b01b73
test(desktop): stabilize app dock acceptance
gmhschn Sep 9, 2026
e181b82
test(desktop): seal app dock child cleanup
gmhschn Sep 9, 2026
7e221d3
feat(app): add app dock tab controls
gmhschn Sep 9, 2026
3feec9a
feat(app): complete app dock tab menu
gmhschn Sep 9, 2026
b9d6529
fix(app): scope app dock controls
gmhschn Sep 9, 2026
6b1e562
fix(app): type tab keyboard target
gmhschn Sep 9, 2026
a644a57
fix(desktop): emit app dock popup tab event
gmhschn Sep 9, 2026
0ad4534
feat(app): add app dock tab controls
gmhschn Sep 9, 2026
c9a9e69
test(desktop): cover HTTPS app dock popup
gmhschn Sep 9, 2026
eaede26
fix(desktop): validate popup event generation
gmhschn Sep 9, 2026
b6cea1f
test(desktop): stabilize app dock acceptance timing
gmhschn Sep 9, 2026
fe7e6bf
fix(app): dismiss app dock tab menu
gmhschn Sep 9, 2026
eea5766
fix(desktop): honor visual app dock tab order
gmhschn Sep 9, 2026
abaee11
fix(desktop): align app dock visual tab closure
gmhschn Sep 9, 2026
c2c7aef
refactor(desktop): freeze app dock S2 contract
gmhschn Sep 9, 2026
268a7ea
fix(app): align app dock event identity
gmhschn Sep 9, 2026
7a276cc
test(desktop): enforce app dock S2 contract
gmhschn Sep 9, 2026
c59bcf8
fix(desktop): validate app dock event identities
gmhschn Sep 9, 2026
3a9b433
feat(desktop): persist app dock profiles
gmhschn Sep 9, 2026
1f6a945
test(desktop): cover app dock registry restarts
gmhschn Sep 9, 2026
89b9a3d
test(desktop): fix app dock restart harness
gmhschn Sep 9, 2026
f3dc8ce
test(desktop): preserve deleted app dock tombstones
gmhschn Sep 9, 2026
661fb18
feat(desktop): persist app dock manifest
gmhschn Sep 9, 2026
3e2f026
feat(app): persist dock UI through manifest
gmhschn Sep 9, 2026
73a6832
fix(desktop): type app dock manifest revisions
gmhschn Sep 9, 2026
59bd8d3
feat(desktop): recover crashed app dock tabs
gmhschn Sep 9, 2026
7ddd3a8
feat(app): recover crashed dock tabs
gmhschn Sep 9, 2026
b9e48e1
test(desktop): cover app dock crash recovery
gmhschn Sep 9, 2026
f6a23f7
test(desktop): target attached crash view
gmhschn Sep 9, 2026
9c538ac
fix(desktop): bound app dock native resources
gmhschn Sep 9, 2026
f6e77c5
fix(desktop): rely on app dock inactive budget
gmhschn Sep 9, 2026
d1f0c1a
test(desktop): strengthen app dock resource limits
gmhschn Sep 9, 2026
17bb38b
feat(desktop): guard app dock devtools
gmhschn Sep 9, 2026
c062230
fix(compaction): contain missing provider error
gmhschn Sep 10, 2026
be8fcfe
merge: integrate fork dev into app dock
gmhschn Sep 10, 2026
3ee8c05
fix(app): repair locale and prompt submit tests
gmhschn Sep 10, 2026
311445f
fix(pty): allow exited sessions via /api/pty/:id handler
gmhschn Sep 14, 2026
941082b
feat(desktop): app-dock RPC bridge + LLM tools
gmhschn Sep 29, 2026
7021f59
feat(plugin): register AppDockPlugin for desktop client
gmhschn Sep 29, 2026
43a6b8b
fix(app-dock): typo buildPDFScript, fix layoutBounds indentation
gmhschn Sep 29, 2026
2cd925c
fix(app-dock): apply browser script fixes (buildPDFScript, buildFrame…
gmhschn Sep 29, 2026
76f8028
refactor(app-dock-rpc): class-based with reset, fix test cleanup
gmhschn Sep 29, 2026
07ff9cb
fix(app-dock-rpc): test stability
gmhschn Sep 29, 2026
3d437ac
fix(app-dock): restrict tool bridge access
gmhschn Sep 29, 2026
afdc50d
fix(app-dock): harden bridge ownership
gmhschn Sep 29, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
75 changes: 75 additions & 0 deletions .github/workflows/corpus-regen.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
name: corpus-regen

on:
push:
branches: [dev, 'feat/**']
pull_request:
branches: [dev, 'feat/**']
workflow_dispatch:

concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true

jobs:
corpus-regen:
name: corpus regen check
runs-on: blacksmith-4vcpu-ubuntu-2404
steps:
- name: Checkout repository
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5

- name: Setup Bun
uses: ./.github/actions/setup-bun

- name: Regenerate corpus
working-directory: packages/desktop/docs/corpus/app-dock
run: node generate-corpus.mjs

- name: Verify corpus up-to-date
run: git diff --exit-code

- name: Lint corpus source
run: bunx oxlint packages/desktop/docs/corpus/app-dock/generate-corpus.mjs

typecheck:
name: typecheck
runs-on: blacksmith-4vcpu-ubuntu-2404
steps:
- name: Checkout repository
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5

- name: Setup Bun
uses: ./.github/actions/setup-bun

- name: Run typecheck
run: bun typecheck

lint:
name: lint
runs-on: blacksmith-4vcpu-ubuntu-2404
steps:
- name: Checkout repository
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5

- name: Setup Bun
uses: ./.github/actions/setup-bun

- name: Run oxlint
run: bunx oxlint packages/desktop/src/main/app-dock-live.test.ts packages/desktop/src/main/app-dock-rpc.ts packages/desktop/src/main/server.ts

live-test:
name: live test (macOS)
runs-on: macos-14
steps:
- name: Checkout repository
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5

- name: Setup Bun
uses: ./.github/actions/setup-bun

- name: Run live harness
working-directory: packages/desktop
run: timeout 300 bun run src/main/app-dock-live.test.ts
env:
ELECTRON_DISABLE_SECURITY_WARNINGS: "true"
62 changes: 62 additions & 0 deletions packages/app/src/app.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -272,6 +272,68 @@ declare global {
deepLinks?: string[]
}
api?: {
appDockOpen?: (
url: string,
bounds: { x: number; y: number; width: number; height: number },
profile?: string,
) => Promise<{ id: string; tabID: string; generation: number; url: string }>
appDockDeleteProfile?: (profileID: string) => Promise<void>
appDockResize?: (bounds: { x: number; y: number; width: number; height: number }) => Promise<void>
appDockHide?: () => Promise<void>
appDockClose?: () => Promise<void>
appDockCloseTab?: (id: string) => Promise<void>
appDockSelect?: (id: string, bounds: { x: number; y: number; width: number; height: number }) => Promise<void>
appDockNavigate?: (id: string, url: string) => Promise<void>
appDockCommand?: (id: string, command: "back" | "forward" | "reload") => Promise<void>
appDockEvent?: (
callback: (
event:
| {
type: "state"
payload: {
tabID: string
generation: number
url: string
title: string
favicon?: string
loading: boolean
audible: boolean
}
}
| {
type: "download"
payload: {
id: string
tabID: string
generation: number
filename: string
receivedBytes: number
totalBytes: number
state: "progressing" | "paused" | "completed" | "cancelled" | "interrupted"
}
}
| { type: "fullscreen"; payload: { identity: { tabID: string; generation: number }; enabled: boolean } }
| {
type: "navigation-error"
payload: { identity: { tabID: string; generation: number }; code: "blocked" | "failed"; url: string }
},
) => void,
) => () => void
appDockFind?: (id: string, text: string, forward: boolean) => Promise<number>
appDockStopFind?: (id: string) => Promise<void>
appDockFindResult?: (
callback: (result: {
tabID: string
requestID: number
activeMatchOrdinal: number
matches: number
finalUpdate: boolean
}) => void,
) => () => void
appDockZoom?: (id: string, factor?: number) => Promise<number>
appDockCancelDownload?: (id: string) => Promise<void>
appDockOpenDownload?: (id: string) => Promise<void>
appDockFullscreen?: (id: string, enabled: boolean) => Promise<void>
setTitlebar?: (theme: { mode: "light" | "dark"; scheme?: "system" | "light" | "dark" }) => Promise<void>
exportDebugLogs?: () => Promise<string>
}
Expand Down
24 changes: 24 additions & 0 deletions packages/app/src/components/dialog-connect-provider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -804,9 +804,11 @@ function ProviderConnection(props: {

function ApiAuthView() {
let apiKey: HTMLInputElement | undefined
let labelInput: HTMLInputElement | undefined
const errorID = createUniqueId()
const [formStore, setFormStore] = createStore({
value: "",
label: "",
error: undefined as string | undefined,
})

Expand All @@ -821,6 +823,7 @@ function ProviderConnection(props: {
const form = e.currentTarget as HTMLFormElement
const formData = new FormData(form)
const apiKey = formData.get("apiKey") as string
const label = formData.get("label") as string

if (!apiKey?.trim()) {
setFormStore("error", language.t("provider.connect.apiKey.required"))
Expand All @@ -832,6 +835,7 @@ function ProviderConnection(props: {
integrationID: props.provider,
location: location(),
key: apiKey,
label: label || undefined,
})
await complete()
}
Expand Down Expand Up @@ -875,6 +879,17 @@ function ProviderConnection(props: {
onInput={(event) => setFormStore("value", event.currentTarget.value)}
/>
</label>
<label class="flex w-full flex-col gap-1 font-[530] leading-4 text-v2-text-text-base">
{language.t("provider.connect.apiKey.labelOptional")}
<TextInputV2
ref={labelInput}
class="!w-full"
name="label"
placeholder={language.t("provider.connect.apiKey.labelPlaceholder")}
value={formStore.label}
onInput={(event) => setFormStore("label", event.currentTarget.value)}
/>
</label>
<Show when={formStore.error}>
{(error) => (
<div id={errorID} role="alert" class="-mt-4 text-xs text-v2-state-fg-danger">
Expand Down Expand Up @@ -924,6 +939,15 @@ function ProviderConnection(props: {
validationState={formStore.error ? "invalid" : undefined}
error={formStore.error}
/>
<TextField
ref={labelInput}
type="text"
label={language.t("provider.connect.apiKey.labelOptional")}
placeholder={language.t("provider.connect.apiKey.labelPlaceholder")}
name="label"
value={formStore.label}
onChange={(v) => setFormStore("label", v)}
/>
<Button class="w-auto" type="submit" size="large" variant="primary">
{language.t("common.continue")}
</Button>
Expand Down
98 changes: 98 additions & 0 deletions packages/app/src/components/dialog-subagent-models.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
import { Dialog } from "@opencode-ai/ui/dialog"
import { List } from "@opencode-ai/ui/list"
import { Switch } from "@opencode-ai/ui/switch"
import { Tooltip } from "@opencode-ai/ui/tooltip"
import { Show, type Component } from "solid-js"
import { useLocal } from "@/context/local"
import { useLanguage } from "@/context/language"
import { useServerSDK } from "@/context/server-sdk"
import { useSync } from "@/context/sync"
import { ModelTooltip } from "./model-tooltip"
import { effectiveModelState, toggleModelRules, type ModelRule } from "./subagent-model-rules"
import { draftVersion, modelKey, pendingSelection, togglePending } from "./draft-subagent-models"

type ModelItem = ReturnType<ReturnType<typeof useLocal>["model"]["list"]>[number]

export const DialogSubagentModels: Component<{ sessionID?: string; directory: string }> = (props) => {
const local = useLocal()
const language = useLanguage()
const serverSDK = useServerSDK()
const sync = useSync()

const sessionRules = (): ModelRule[] => {
if (!props.sessionID) return []
return (sync().session.get(props.sessionID)?.permission ?? []) as ModelRule[]
}

const isAllowed = (providerID: string, modelID: string) => {
// Drafts have no session yet: truth is the pending selection.
// draftVersion() subscribes so plain-Set mutations re-render.
if (!props.sessionID) {
draftVersion()
return pendingSelection(props.directory).has(modelKey(providerID, modelID))
}
return effectiveModelState(sessionRules(), providerID, modelID) === "allow"
}

const setAllowed = async (providerID: string, modelID: string, allow: boolean) => {
if (!props.sessionID) {
togglePending(props.directory, providerID, modelID)
return
}
if ((await serverSDK().protocol) !== "v1") return
await serverSDK().client.session.update({
sessionID: props.sessionID,
directory: props.directory,
permission: toggleModelRules(sessionRules(), providerID, modelID, allow),
})
}

return (
<Dialog
title={language.t("dialog.subagentModels.title")}
description={language.t("dialog.subagentModels.description")}
>
<List
class="px-3"
search={{ placeholder: language.t("dialog.model.search.placeholder"), autofocus: true }}
emptyMessage={language.t("dialog.model.empty")}
key={(x) => `${x?.provider?.id}:${x?.id}`}
items={local.model
.list()
.filter((m) => local.model.visible({ modelID: m.id, providerID: m.provider.id }))}
filterKeys={["provider.name", "name", "id"]}
sortBy={(a, b) => a.name.localeCompare(b.name)}
groupBy={(x) => x.provider.name}
groupHeader={(group) => <span>{group.items[0]?.provider.name}</span>}
itemWrapper={(item, node) => (
<Tooltip
class="w-full"
placement="right-start"
gutter={12}
openDelay={0}
value={<ModelTooltip model={item} />}
>
{node}
</Tooltip>
)}
>
{(item: ModelItem) => (
<div class="w-full flex items-center gap-x-2 text-13-regular">
<span class="truncate">{item.name}</span>
<Switch
class="-mr-1 ml-auto"
checked={isAllowed(item.provider.id, item.id)}
onChange={(checked) => void setAllowed(item.provider.id, item.id, checked)}
hideLabel
>
{item.name}
</Switch>
</div>
)}
</List>
<div class="text-12-regular text-text-weak px-3 pb-1">
{language.t("dialog.subagentModels.hint")}
</div>
</Dialog>
)
}
33 changes: 33 additions & 0 deletions packages/app/src/components/draft-subagent-models.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import { describe, expect, test } from "bun:test"
import { clearPending, modelKey, pendingRules, pendingSelection, togglePending } from "./draft-subagent-models"

const DIR = "/tmp/draft-test"

describe("draft subagent models", () => {
test("toggle adds and removes selections", () => {
clearPending(DIR)
expect(togglePending(DIR, "openrouter", "x")).toBe(true)
expect(pendingSelection(DIR).has(modelKey("openrouter", "x"))).toBe(true)
expect(togglePending(DIR, "openrouter", "x")).toBe(false)
expect(pendingSelection(DIR).size).toBe(0)
clearPending(DIR)
})

test("pendingRules emits deny baseline plus allows", () => {
clearPending(DIR)
expect(pendingRules(DIR)).toEqual([])
togglePending(DIR, "openrouter", "x")
togglePending(DIR, "groq", "y")
expect(pendingRules(DIR)).toEqual([
{ permission: "task", pattern: "*/*", action: "deny" },
{ permission: "task", pattern: "openrouter/x", action: "allow" },
{ permission: "task", pattern: "groq/y", action: "allow" },
])
clearPending(DIR)
expect(pendingRules(DIR)).toEqual([])
})

test("modelKey joins provider and model", () => {
expect(modelKey("openrouter", "a/b")).toBe("openrouter/a/b")
})
})
57 changes: 57 additions & 0 deletions packages/app/src/components/draft-subagent-models.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import type { ModelRule } from "./subagent-model-rules"

import { createSignal } from "solid-js"

/** Pending subagent-model selections for drafts (no session exists yet).
Keyed by directory; flushed into session permission rules right after
the session is created, before the first prompt is sent — so there is
no race with the first task call. Entries are tiny bounded sets; they
are cleared on flush. */
const pending = new Map<string, Set<string>>()

const [version, bumpVersion] = createSignal(0)
/** Reactive clock: read inside memos/render so plain-Set mutations propagate. */
export const draftVersion = version

export const modelKey = (providerID: string, modelID: string) => `${providerID}/${modelID}`

export function pendingSelection(directory: string): Set<string> {
let set = pending.get(directory)
if (!set) {
set = new Set()
pending.set(directory, set)
}
return set
}

export function togglePending(directory: string, providerID: string, modelID: string): boolean {
const set = pendingSelection(directory)
const key = modelKey(providerID, modelID)
let next: boolean
if (set.has(key)) {
set.delete(key)
next = false
} else {
set.add(key)
next = true
}
bumpVersion((v) => v + 1)
return next
}

export function clearPending(directory: string) {
if (pending.delete(directory)) bumpVersion((v) => v + 1)
}

/** Builds the rule batch for a fresh selection: deny-all baseline plus one
allow per selected model (server merges, last match wins). */
export function pendingRules(directory: string): ModelRule[] {
const selected = pendingSelection(directory)
if (selected.size === 0) return []
return [
{ permission: "task", pattern: "*/*", action: "deny" as const },
...[...selected].map(
(key): ModelRule => ({ permission: "task", pattern: key, action: "allow" as const }),
),
]
}
Loading
Loading