Add /code product showcase; refresh siGit Code Cloud skills

- pages#code at /code: siGit Code showcase (on-device, ACP, cloud tiers, install, slash commands), matching the design system; Code link in the navbar - skills: document the decoupled login/model selection, the always-shown /models cloud tiers with sign-in gated at selection, slash account commands, and the onde-cloud SelfHosted/Upstream backend rename Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

Seto Elkahfi committed Jun 22, 2026 at 22:49 UTC 6f22e40ab4f800d1c7c5ec73fa05fa77029ca64d
6 files changed +222 -38
.agents/skills/sigit-code-cloud/SKILL.md
+54 -37
@@ -46,42 +46,55 @@ The generic engine is also what enables the **BYO-endpoint escape hatch**: an
46 advanced user or enterprise can point siGit Code at their own OpenAI/Azure
47 endpoint. That is a power-user override, **not** the front door.
48
49 -## Backend selection (precedence)
49 +## Backend selection: identity and model choice are separate
50
51 -Resolved in `sigit/src/provider.rs::active_provider()`, first match wins:
51 +Being signed in does **not** change which model runs. Login is auth; the model
52 +is the user's explicit choice in `/models`. The startup default
53 +(`provider::active_provider()`) is local-first, with one exception:
54
53 -1. **Override (power user / BYO):** `OPENAI_BASE_URL` + `OPENAI_API_KEY` env vars,
54 - or the active profile in `~/.config/sigit/providers.toml`.
55 -2. **siGit Code Cloud (default product):** when logged in (`sigit login` stored a
56 - token). Baked-in endpoint, token auth, neutral tier. The user provides nothing
57 - but their login.
58 -3. **On-device:** not logged in, no override → runs locally via `onde::ChatEngine`.
55 +1. **Override (power user / BYO):** `OPENAI_BASE_URL` + `OPENAI_API_KEY`, or the
56 + active profile in `~/.config/sigit/providers.toml`. This is the only thing that
57 + changes the *startup* backend.
58 +2. **On-device by default:** no override means local, signed in or not.
59
60 -A missing piece in the override path is loud, not silent: `OPENAI_BASE_URL` set
61 -without `OPENAI_API_KEY` logs a warning and falls back to on-device, rather than
62 -silently looking like "the cloud didn't work."
60 +The user then picks a model in `/models` (see below), which swaps the running
61 +backend in place. `OPENAI_BASE_URL` set without `OPENAI_API_KEY` logs a warning
62 +and stays on-device, rather than silently looking like "the cloud didn't work."
63
64 -## Quality tiers (neutral by design)
64 +## `/models`: on-device models and cloud tiers together
65
66 -The user picks a tier, never a model id or provider. `SIGIT_TIER` selects it
67 -(default `balanced`); `provider.rs::tier_to_model` maps it:
66 +`/models` always lists the on-device models **and** the siGit Code Cloud tiers
67 +(`Fast`/`Balanced`/`Large`), so the cloud option is discoverable to everyone.
68 +Access is gated at **selection**, not visibility:
69
69 -| Tier | Wire model id (Onde Cloud) |
70 -|------|----------------------------|
71 -| `fast` | `onde-fast` |
72 -| `balanced` | `onde-balanced` |
73 -| `large` | `onde-large` |
70 +- **Pick a cloud tier while signed in** → the backend hot-swaps to that tier
71 + immediately (no restart). `provider::cloud_tier_provider(tier)` builds the
72 + client from the stored token.
73 +- **Pick a cloud tier while signed out** → a prompt: sign in with `/login`, or
74 + create an account at sigit.si. The backend does not change.
75 +- **Pick an on-device model** → loads and routes locally, as before.
76
75 -The UI shows `siGit Code Cloud · Balanced`, never the model id or "Anthropic".
76 -`ProviderConfig.display_name` carries this label; the title bar uses it, and
77 -`InferenceBackend::is_remote()` ensures a persisted *local* model name can never
78 -override the cloud label.
77 +The cloud tiers are real picker entries backed by a synthetic
78 +`GgufModelConfig` whose id is `sigit-cloud:<tier>` (no `/`, so it never collides
79 +with a HuggingFace id and on-device matching code stays inert). `cloud_tier:
80 +Option<String>` on `ModelPickerItem` distinguishes them. The tier maps to a wire
81 +model id via `provider::tier_to_model` (`fast`→`onde-fast`, etc.); the UI shows
82 +`siGit Code Cloud · Balanced`, never the model id or "Anthropic".
83 +
84 +Cloud tiers are a TUI-picker feature today. The ACP session-config paths use
85 +`models::local_picker_items()` (cloud filtered out), because cloud inference over
86 +ACP still needs the trait rewire (see Open items).
87
88 ## Account / auth (via sigit.si)
89
82 -`sigit login | logout | whoami` are plain CLI verbs dispatched in
83 -`sigit/src/main.rs` before the TUI/ACP split, implemented in
84 -`sigit/src/account.rs`.
90 +Account management is exposed as **slash commands** (`/login <email> <password>`,
91 +`/logout`, `/whoami`) so it works inside an ACP session (Zed) and the terminal
92 +TUI alike, since siGit Code has no separate place to run a CLI verb in those
93 +contexts. Both surfaces parse `/`-prefixed input and call the I/O-free core
94 +functions in `sigit/src/account.rs` (`authenticate`, `end_session`,
95 +`status_line`). Slash input is not echoed to the transcript, so the password
96 +does not persist there. `/login` only signs in; it does not change the active
97 +model. To use the cloud after signing in, pick a tier in `/models`.
98
99 - **Trust boundary is `sigit.si`.** It holds the confidential smbCloud
100 `app_secret` server-side and returns the client only a **bearer token**. The
@@ -89,11 +102,13 @@ override the cloud label.
102 `smbcloud-auth` skill's public-client rule).
103 - **Base URL:** `$SIGIT_API_URL`, else `https://sigit.si` (dev:
104 `http://localhost:8088`).
92 -- **Endpoints (sigit.si `/api/v1`, token-based):** `POST /users/sign_in` →
93 - `access_token`; `GET /me`; `DELETE /users/sign_out`. These reuse the planned
94 - `/api/v1` JSON surface in sigit-si (see the `smbcloud-auth` skill); confirm the
95 - exact routes/response shapes against that app and keep them in sync with the
96 - desktop `account/command_*` set.
105 +- **Endpoints (sigit.si `/api/v1`, token-based):** `POST /api/v1/auth/sign_in`
106 + returns an `AccountStatus` (`"NotFound"` | `{"Ready":{"access_token":…}}` |
107 + `{"Incomplete":{"status":<u32>}}`); `GET /api/v1/me` returns
108 + `{id,email,created_at,updated_at}`; `DELETE /api/v1/auth/sign_out` returns 204.
109 + Failures return `ErrorResponse {error_code, message}`. These shapes are a shared
110 + contract with the desktop client; keep them in sync (see `smbcloud-auth`). The
111 + controllers live at `app/controllers/api/v1/{sessions,me}_controller.rb`.
112 - **Credential store:** `sigit/src/credentials.rs` writes
113 `~/.config/sigit/credentials.toml` (`$SIGIT_CONFIG_DIR` override), mode `0600`,
114 holding `access_token` (+ email for display).
@@ -125,9 +140,11 @@ Repo: `onde-cloud` (Rust/axum, OpenAI-compatible).
140 cd ~/Repositories/onde-cloud && set -a && . ./.env && set +a && ./target/debug/onde-cloud
141 # → listens on $PORT (e.g. 8090); DEV_MODE=true accepts any bearer
142
128 -# 2a. Product path: point siGit Code Cloud at the local API and log in
129 -SIGIT_CLOUD_URL=http://localhost:8090/v1 SIGIT_API_URL=http://localhost:8088 \
130 - sigit login # stores a token; then `sigit` defaults to the cloud
143 +# 2a. Product path: point siGit Code Cloud at the local API, start sigit
144 +SIGIT_CLOUD_URL=http://localhost:8090/v1 SIGIT_API_URL=http://localhost:8088 sigit
145 +# then inside the session:
146 +# /login <email> <password> # sign in
147 +# /models # pick a siGit Code Cloud tier (hot-swaps)
148
149 # 2b. BYO/override path (no login needed): explicit endpoint
150 OPENAI_BASE_URL=http://localhost:8090/v1 OPENAI_API_KEY=dev:dev SIGIT_TIER=fast sigit
@@ -140,7 +157,7 @@ while testing to keep upstream spend low.
157
158 ## Key files
159
143 -- `sigit/src/account.rs`: `login`/`logout`/`whoami`, sigit.si `/api/v1` client.
160 +- `sigit/src/account.rs`: account core (`authenticate`/`end_session`/`status_line`), sigit.si `/api/v1` client. Surfaced as `/login`, `/logout`, `/whoami`.
161 - `sigit/src/credentials.rs`: local token store.
162 - `sigit/src/provider.rs`: backend precedence, tiers, cloud default.
163 - `sigit/src/backend.rs`: `InferenceBackend` trait, `LocalBackend`, `OpenAiBackend`.
@@ -152,8 +169,8 @@ while testing to keep upstream spend low.
169 `app_id:app_secret`; the product needs it to accept the sigit.si-issued user
170 token (sigit.si fronts/mints inference auth, mirroring the `git_token` exchange
171 in `sigit-app`). Until then, dev relies on `DEV_MODE`.
155 -- **`sigit.si/api/v1` routes** are assumed from the desktop contract, confirm and
156 - keep in sync with the `smbcloud-auth` skill.
172 +- **`sigit.si/api/v1` auth routes exist** (`sessions`/`me` controllers). The
173 + remaining gap is the inference-token link above, not the account endpoints.
174 - **Usage/billing:** `onde-cloud` returns zeroed `usage`; wire Anthropic's
175 returned token counts through for metering.
176 - **Tool-call id fingerprint:** Onde Cloud passes Anthropic's `toolu_…` tool-call
.agents/skills/sigit-code/SKILL.md
+2 -1
@@ -24,7 +24,8 @@ runs over ACP in editors like Zed and as an interactive terminal TUI. The repo i
24 - `provider.rs`: backend selection and quality tiers. Precedence: an explicit
25 override (`OPENAI_BASE_URL` + `OPENAI_API_KEY`, or `~/.config/sigit/providers.toml`),
26 then siGit Code Cloud when logged in, then on-device.
27 -- `account.rs`: `login` / `logout` / `whoami` against the account API.
27 +- `account.rs`: account core against the account API, surfaced as the `/login`,
28 + `/logout`, `/whoami` slash commands (both TUI and ACP).
29 - `credentials.rs`: the local session-token store.
30 - `tools.rs`: tool implementations and their JSON schemas.
31 - `models.rs`, `setup.rs`: local model picker and HuggingFace cache setup.
app/controllers/pages_controller.rb
+2
@@ -6,6 +6,8 @@ class PagesController < ApplicationController
6 end
7 end
8
9 + def code; end
10 +
11 def about; end
12
13 def contact; end
app/views/pages/code.html.erb new
+162
@@ -0,0 +1,162 @@
1 +<% content_for :title, "siGit Code" %>
2 +
3 +<div class="min-h-screen bg-surface-900">
4 + <%# ── Hero ── %>
5 + <section class="border-b border-surface-600 bg-surface-800">
6 + <div class="max-w-6xl mx-auto px-4 sm:px-6 py-20 sm:py-28">
7 + <div class="max-w-3xl">
8 + <span class="inline-flex items-center gap-2 rounded-sm border border-surface-500 bg-surface-700 px-3 py-1 text-xs font-medium text-gray-300">
9 + <span class="h-1.5 w-1.5 rounded-full bg-brand-500"></span>
10 + siGit Code
11 + </span>
12 +
13 + <h1 class="mt-6 max-w-2xl text-4xl font-semibold tracking-tight text-gray-100 sm:text-6xl">
14 + A coding agent that runs on your machine.
15 + </h1>
16 +
17 + <p class="mt-6 max-w-xl text-base leading-7 text-gray-400 sm:text-lg">
18 + siGit Code is a local AI coding agent. It works in your editor over the
19 + Agent Client Protocol and runs on-device by default, so your code stays
20 + on your machine. When a task needs a bigger model, sign in and switch to
21 + siGit Code Cloud.
22 + </p>
23 +
24 + <div class="mt-10 flex flex-col gap-3 sm:flex-row">
25 + <a href="https://getsigit.5mb.app/" class="btn-primary px-6 py-3 text-sm justify-center" target="_blank" rel="noopener noreferrer">Get siGit Code</a>
26 + <a href="#install" class="btn-secondary px-6 py-3 text-sm justify-center">Install from your terminal</a>
27 + </div>
28 + </div>
29 + </div>
30 + </section>
31 +
32 + <%# ── What it is ── %>
33 + <section class="max-w-6xl mx-auto px-4 sm:px-6 py-16 sm:py-20">
34 + <div class="grid grid-cols-1 gap-px overflow-hidden rounded-sm border border-surface-500 bg-surface-500 sm:grid-cols-3">
35 + <div class="bg-surface-800 p-8 sm:p-10">
36 + <div class="mb-6 flex h-10 w-10 items-center justify-center rounded-sm border border-surface-500 bg-surface-700 text-brand-500">
37 + <span class="text-lg font-semibold">01</span>
38 + </div>
39 + <h2 class="text-lg font-semibold text-gray-100">On-device by default</h2>
40 + <p class="mt-3 text-sm leading-6 text-gray-400">
41 + Inference runs locally, on Apple silicon and other hardware. Your code
42 + never leaves your machine unless you pick a cloud tier, and it costs
43 + nothing to run.
44 + </p>
45 + </div>
46 +
47 + <div class="bg-surface-800 p-8 sm:p-10">
48 + <div class="mb-6 flex h-10 w-10 items-center justify-center rounded-sm border border-surface-500 bg-surface-700 text-brand-500">
49 + <span class="text-lg font-semibold">02</span>
50 + </div>
51 + <h2 class="text-lg font-semibold text-gray-100">Lives in your editor</h2>
52 + <p class="mt-3 text-sm leading-6 text-gray-400">
53 + siGit Code speaks the Agent Client Protocol, so it runs in Zed and other
54 + ACP editors, or as a terminal chat. It is the same agent either way.
55 + </p>
56 + </div>
57 +
58 + <div class="bg-surface-800 p-8 sm:p-10">
59 + <div class="mb-6 flex h-10 w-10 items-center justify-center rounded-sm border border-surface-500 bg-surface-700 text-brand-500">
60 + <span class="text-lg font-semibold">03</span>
61 + </div>
62 + <h2 class="text-lg font-semibold text-gray-100">Tools and git, built in</h2>
63 + <p class="mt-3 text-sm leading-6 text-gray-400">
64 + It reads files, runs commands, searches the codebase, and handles git
65 + itself, instead of telling you what to type. That keeps you in the work.
66 + </p>
67 + </div>
68 + </div>
69 + </section>
70 +
71 + <%# ── siGit Code Cloud ── %>
72 + <section class="border-y border-surface-600 bg-surface-800">
73 + <div class="max-w-6xl mx-auto px-4 sm:px-6 py-16 sm:py-20">
74 + <div class="max-w-2xl">
75 + <h2 class="text-2xl font-semibold tracking-tight text-gray-100 sm:text-3xl">siGit Code Cloud</h2>
76 + <p class="mt-4 text-sm leading-7 text-gray-400 sm:text-base">
77 + Use the local model for everyday work, and switch to a hosted tier when a
78 + task calls for a larger model. You pick a tier in <code class="rounded-sm bg-surface-700 px-1.5 py-0.5 text-gray-300">/models</code>,
79 + and your account decides which ones you can use. You never paste an API key.
80 + </p>
81 + </div>
82 +
83 + <div class="mt-10 grid grid-cols-1 gap-4 sm:grid-cols-3">
84 + <div class="card p-6">
85 + <h3 class="text-base font-semibold text-gray-100">Fast</h3>
86 + <p class="mt-2 text-sm leading-6 text-gray-400">Quick edits and snappy back-and-forth.</p>
87 + </div>
88 + <div class="card p-6">
89 + <h3 class="text-base font-semibold text-gray-100">Balanced</h3>
90 + <p class="mt-2 text-sm leading-6 text-gray-400">The everyday tier for most agent work.</p>
91 + </div>
92 + <div class="card p-6">
93 + <h3 class="text-base font-semibold text-gray-100">Large</h3>
94 + <p class="mt-2 text-sm leading-6 text-gray-400">The heaviest tier, for hard multi-step problems.</p>
95 + </div>
96 + </div>
97 +
98 + <div class="mt-8 flex flex-col gap-3 sm:flex-row">
99 + <% if signed_in? %>
100 + <%= link_to "Manage your account", settings_path, class: "btn-secondary px-6 py-3 text-sm justify-center" %>
101 + <% else %>
102 + <%= link_to "Create an account", signup_path, class: "btn-primary px-6 py-3 text-sm justify-center" %>
103 + <%= link_to "Sign in", signin_path, class: "btn-secondary px-6 py-3 text-sm justify-center" %>
104 + <% end %>
105 + </div>
106 + </div>
107 + </section>
108 +
109 + <%# ── Install ── %>
110 + <section id="install" class="max-w-6xl mx-auto px-4 sm:px-6 py-16 sm:py-20">
111 + <div class="grid grid-cols-1 gap-8 lg:grid-cols-2">
112 + <div>
113 + <h2 class="text-2xl font-semibold tracking-tight text-gray-100 sm:text-3xl">Install</h2>
114 + <p class="mt-4 text-sm leading-7 text-gray-400">
115 + Grab it however you ship Rust, Node, or Python tools, then run
116 + <code class="rounded-sm bg-surface-700 px-1.5 py-0.5 text-gray-300">sigit</code>
117 + in a project.
118 + </p>
119 + <pre class="mt-6 overflow-x-auto rounded-sm border border-surface-500 bg-surface-800 p-4 font-mono text-sm leading-7 text-gray-300"><span class="text-gray-500"># Rust</span>
120 +cargo install sigit
121 +
122 +<span class="text-gray-500"># Node</span>
123 +npm install -g @smbcloud/sigit
124 +
125 +<span class="text-gray-500"># Python</span>
126 +pip install sigit-code
127 +
128 +<span class="text-gray-500"># then, in any repository</span>
129 +sigit</pre>
130 + </div>
131 +
132 + <div>
133 + <h2 class="text-2xl font-semibold tracking-tight text-gray-100 sm:text-3xl">Commands</h2>
134 + <p class="mt-4 text-sm leading-7 text-gray-400">
135 + Everything is a slash command, in the editor or the terminal.
136 + </p>
137 + <dl class="mt-6 divide-y divide-surface-600 overflow-hidden rounded-sm border border-surface-500 bg-surface-800 text-sm">
138 + <div class="flex gap-4 px-4 py-3">
139 + <dt class="w-40 shrink-0 font-mono text-brand-500">/models</dt>
140 + <dd class="text-gray-400">List models and tiers, and switch between them.</dd>
141 + </div>
142 + <div class="flex gap-4 px-4 py-3">
143 + <dt class="w-40 shrink-0 font-mono text-brand-500">/login</dt>
144 + <dd class="text-gray-400">Sign in to unlock siGit Code Cloud.</dd>
145 + </div>
146 + <div class="flex gap-4 px-4 py-3">
147 + <dt class="w-40 shrink-0 font-mono text-brand-500">/whoami</dt>
148 + <dd class="text-gray-400">Show the signed-in account.</dd>
149 + </div>
150 + <div class="flex gap-4 px-4 py-3">
151 + <dt class="w-40 shrink-0 font-mono text-brand-500">/logout</dt>
152 + <dd class="text-gray-400">Sign out on this machine.</dd>
153 + </div>
154 + <div class="flex gap-4 px-4 py-3">
155 + <dt class="w-40 shrink-0 font-mono text-brand-500">/help</dt>
156 + <dd class="text-gray-400">List every command.</dd>
157 + </div>
158 + </dl>
159 + </div>
160 + </div>
161 + </section>
162 +</div>
app/views/shared/_navbar.html.erb
+1
@@ -8,6 +8,7 @@
8 <% end %>
9
10 <div class="hidden sm:flex items-center gap-1">
11 + <%= link_to "Code", code_path, class: "nav-link px-2 py-1 rounded hover:bg-surface-600" %>
12 <%= link_to "Models", models_path, class: "nav-link px-2 py-1 rounded hover:bg-surface-600" %>
13 <%= link_to "Repos", repos_path, class: "nav-link px-2 py-1 rounded hover:bg-surface-600" %>
14 </div>
config/routes.rb
+1
@@ -3,6 +3,7 @@ Rails.application.routes.draw do
3
4 # Static pages — declared before the "/:username" matcher so these literal
5 # slugs aren't read as profiles.
6 + get "/code", to: "pages#code", as: :code
7 get "/about", to: "pages#about", as: :about
8 get "/contact", to: "pages#contact", as: :contact
9 get "/privacy", to: "pages#privacy", as: :privacy