Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
98 changes: 4 additions & 94 deletions .github/workflows/_publish-registry.yml
Original file line number Diff line number Diff line change
Expand Up @@ -102,100 +102,10 @@ jobs:
cat iii-engine.log || true
exit 1

# The harness bundle contains sub-workers (auth-credentials,
# provider-config) that call `database::execute` during
# registration to CREATE TABLE their backing stores. Without a
# running `iii-database` worker the harness aborts at boot with
# `function_not_found`, so we never reach interface collection.
# Fetch the latest published `database` release binary and start
# it before the harness bundle so the trigger resolves.
#
# Snapshotting the trigger baseline AFTER this step keeps
# `database::*` out of the published harness interface.
- name: Start dependency workers (harness)
if: inputs.worker == 'harness'
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
REPO: ${{ github.repository }}
run: |
set -euo pipefail

# Resolve the most recent non-prerelease `database/v*` tag.
# gh release list returns newest-first; the registry tag we
# publish under is what production consumers actually pull,
# so match it here too.
db_tag=$(gh release list \
--repo "$REPO" \
--limit 100 \
--exclude-drafts \
--exclude-pre-releases \
--json tagName \
--jq '[.[] | select(.tagName | startswith("database/v"))][0].tagName')

if [[ -z "$db_tag" || "$db_tag" == "null" ]]; then
echo "::error::could not resolve latest database/v* release tag"
exit 1
fi
echo "Using database release: $db_tag"

asset_url="https://github.com/$REPO/releases/download/$db_tag/database-x86_64-unknown-linux-gnu.tar.gz"
echo "Fetching database binary: $asset_url"
curl -fsSL "$asset_url" -o /tmp/database-bin.tar.gz

db_dir="/tmp/iii-database"
rm -rf "$db_dir"
mkdir -p "$db_dir"
tar -xzf /tmp/database-bin.tar.gz -C "$db_dir"
chmod +x "$db_dir/database"

# The pool name must match the harness database_name default
# (see harness/src/runtime/storage-config.ts DEFAULT_DATABASE_NAME).
# SQLite is sufficient for interface collection -- no data is
# persisted past the job.
cat > "$db_dir/config.yaml" <<'CFG'
databases:
harness:
url: sqlite:./iii.db
pool:
max: 4
idle_timeout_ms: 30000
acquire_timeout_ms: 5000
CFG

db_log="$PWD/iii-database.log"
pushd "$db_dir" >/dev/null
./database > "$db_log" 2>&1 &
echo "$!" > "$PWD/database.pid"
popd >/dev/null
cp "$db_dir/database.pid" iii-database.pid

# Wait for the database worker to register database::execute
# by issuing a trivial roundtrip. The engine returns
# function_not_found until registration completes.
ready=0
for _ in {1..30}; do
if ! kill -0 "$(cat iii-database.pid)" 2>/dev/null; then
echo "::error::iii-database exited before becoming ready"
tail -n 200 "$db_log" || true
exit 1
fi
if iii trigger 'database::execute' \
--json '{"db":"harness","sql":"SELECT 1","params":[]}' \
>/tmp/iii-database-ping.json 2>/tmp/iii-database-ping.err; then
ready=1
break
fi
sleep 1
done

if [[ "$ready" != "1" ]]; then
echo "::error::iii-database did not register database::execute in time"
cat /tmp/iii-database-ping.err || true
tail -n 200 "$db_log" || true
exit 1
fi
echo "iii-database ready"

# The harness bundle no longer depends on the `database` worker —
# provider credentials/settings + permissions live in the built-in
# `configuration` worker (engine-default-enabled), so no dependency
# worker needs to be started before interface collection.
- name: Snapshot engine trigger types baseline
run: |
set -euo pipefail
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ asset for the host from the workers registry API.
| Worker | Kind | Summary |
|---|---|---|
| [`acp`](acp/) | Rust | Agent Client Protocol surface — stdio JSON-RPC, exposes iii agents as ACP sessions. |
| [`harness`](harness/) | Node | TS port of the iii harness stack — bundles `harness`, `turn-orchestrator`, `approval-gate`, `session`, `hook-fanout`, `auth-credentials`, `models-catalog`, `provider-anthropic`, `provider-openai`, `llm-budget`, and `context-compaction` as one pnpm monorepo. See [`harness/README.md`](harness/README.md). |
| [`harness`](harness/) | Node | TS port of the iii harness stack — bundles `harness` (provider registry + credentials/settings/permissions via the `configuration` worker), `turn-orchestrator`, `approval-gate`, `session`, `hook-fanout`, `models-catalog`, the `provider-*` workers, `llm-budget`, and `context-compaction` as one pnpm monorepo. See [`harness/README.md`](harness/README.md). |
| [`database`](database/) | Rust | PostgreSQL, MySQL, and SQLite client — query, execute, transactions, prepared statements, and change feeds. |
| [`iii-directory`](iii-directory/) | Rust | Engine introspection (functions / triggers / workers), workers-registry proxy, and filesystem-backed skill + prompt reader. |
| [`iii-lsp`](iii-lsp/) | Rust | Language Server for iii function ids, trigger configs, and worker discovery. Autocomplete / hover across JS/TS, Python, Rust. |
Expand Down
136 changes: 82 additions & 54 deletions console/web/src/components/chat/ModelPicker.tsx
Original file line number Diff line number Diff line change
@@ -1,25 +1,18 @@
import * as SelectPrimitive from '@radix-ui/react-select'
import { Settings } from 'lucide-react'
import { useState } from 'react'
import { ProviderSettingsDialog } from '@/components/providers/ProviderSettingsDialog'
import {
ACTIVE_PROVIDERS,
type ActiveProvider,
ENV_VAR_MAP,
} from '@/components/providers/provider-registry'
import { RefreshCw, Settings } from 'lucide-react'
import { Select, type SelectGroup } from '@/components/ui/Select'
import { useConversationsCtxOptional } from '@/lib/conversations-context'
import { cn } from '@/lib/utils'
import {
CATALOG_MODEL_KEY_SEP,
type ModelId,
type ModelOption,
} from '@/types/chat'

const ENV_VAR_BY_ID = new Map(ENV_VAR_MAP)
const ACTIVE_PROVIDER_SET: ReadonlySet<string> = new Set(ACTIVE_PROVIDERS)

function isActiveProvider(id: string): id is ActiveProvider {
return ACTIVE_PROVIDER_SET.has(id)
}
// Deep link to the harness configuration entry in the workers/config editor,
// where api keys + per-provider settings are now edited (the bespoke
// per-provider dialog was retired in favour of the schema-driven form).
const HARNESS_CONFIG_HASH = '#/configuration/workers/harness'

interface ModelPickerProps {
value: ModelId
Expand Down Expand Up @@ -51,64 +44,99 @@ export function ModelPicker({
loading,
className,
}: ModelPickerProps) {
const [settingsProvider, setSettingsProvider] =
useState<ActiveProvider | null>(null)
// Optional: present in the app, absent in isolated Storybook renders.
const ctx = useConversationsCtxOptional()

// Providers present as harness workers (from harness::provider::list).
// Absent in Storybook or before the list resolves, in which case no empty
// provider groups or gears appear until the dynamic list arrives.
const presentIds = ctx?.presentProviders.map((p) => p.id) ?? []
const presentSet = new Set<string>(presentIds)

const pickerOptions =
options.length > 0 ? options : [{ id: value, label: value }]
const safeValue = pickerOptions.some((o) => o.id === value)
? value
: pickerOptions[0].id

// Groups from the registered models, plus an empty group for each present
// provider that has no models yet (present-but-unconfigured) so it still
// shows up with a gear to open its configuration.
const modelGroups = groupByProvider(pickerOptions)
const grouped = new Set(modelGroups.map((g) => g.label))
const emptyGroups: SelectGroup<ModelId>[] = presentIds
.filter((id) => !grouped.has(id))
.map((id) => ({ label: id, options: [] }))
const groups = [...modelGroups, ...emptyGroups].sort((a, b) =>
a.label.localeCompare(b.label),
)

return (
<>
<span className="inline-flex items-center gap-1">
<Select<ModelId>
value={safeValue}
groups={groupByProvider(pickerOptions)}
groups={groups}
onChange={onChange}
disabled={disabled || loading}
aria-label={loading ? 'model (loading catalog)' : 'model'}
aria-busy={loading || undefined}
className={className}
renderGroupHeader={(g) => (
<div className="flex items-center justify-between gap-2 pr-2 pt-2 pb-1">
<SelectPrimitive.Label className="px-3 text-[11px] uppercase tracking-[0.12em] text-ink-faint">
{g.label}
</SelectPrimitive.Label>
{isActiveProvider(g.label) ? (
<button
type="button"
aria-label={`configure ${g.label}`}
title={`configure ${g.label}`}
// Stop Radix Select from interpreting the click as an
// option-pick / outside-click; the gear opens the provider
// settings dialog, which steals focus and closes the
// dropdown naturally.
onPointerDown={(e) => e.stopPropagation()}
onClick={(e) => {
e.preventDefault()
e.stopPropagation()
setSettingsProvider(g.label as ActiveProvider)
}}
className="text-ink-faint hover:text-ink transition-colors p-0.5 -mr-0.5"
>
<Settings size={12} />
</button>
) : null}
</div>
)}
renderGroupHeader={(g) => {
const unconfigured = g.options.length === 0
return (
<div className="flex items-center justify-between gap-2 pr-2 pt-2 pb-1">
<span className="flex min-w-0 items-baseline gap-1.5">
<SelectPrimitive.Label className="px-3 text-[11px] uppercase tracking-[0.12em] text-ink-faint">
{g.label}
</SelectPrimitive.Label>
{unconfigured ? (
<span className="text-[10px] lowercase tracking-normal text-ink-ghost">
not configured
</span>
) : null}
</span>
{presentSet.has(g.label) ? (
<button
type="button"
aria-label={`configure ${g.label}`}
title={`configure ${g.label} in harness configuration`}
// Stop Radix Select from interpreting the click as an
// option-pick / outside-click; navigating to the config
// editor closes the dropdown naturally.
onPointerDown={(e) => e.stopPropagation()}
onClick={(e) => {
e.preventDefault()
e.stopPropagation()
window.location.hash = HARNESS_CONFIG_HASH
}}
className="text-ink-faint hover:text-ink transition-colors p-0.5 -mr-0.5"
>
<Settings size={12} />
</button>
) : null}
</div>
)
}}
/>

{settingsProvider ? (
<ProviderSettingsDialog
provider={settingsProvider}
envVar={ENV_VAR_BY_ID.get(settingsProvider) ?? ''}
open
onOpenChange={(open) => {
if (!open) setSettingsProvider(null)
{ctx ? (
<button
type="button"
aria-label="refresh model list"
title="refresh model list from providers"
disabled={ctx.refreshingModels || disabled}
onClick={() => {
void ctx.refreshModels()
}}
/>
className="text-ink-ghost hover:text-ink transition-colors p-1 disabled:opacity-50"
>
<RefreshCw
size={12}
className={cn(ctx.refreshingModels && 'animate-spin')}
aria-hidden
/>
</button>
) : null}
</>
</span>
)
}
Loading
Loading