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
34 changes: 33 additions & 1 deletion doc/openapi.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -739,6 +739,14 @@ paths:
$ref: "#/components/schemas/Project"
"401":
$ref: "#/components/responses/Unauthorized"
"403":
description: |
The caller's account has `local_project_disabled=true`
(cannot create local projects). Admins are exempt.
content:
application/json:
schema:
$ref: "#/components/schemas/Error"
"409":
description: A project with this `host_path` already exists
content:
Expand Down Expand Up @@ -1091,6 +1099,15 @@ paths:
$ref: "#/components/schemas/IndexBeginResponse"
"401":
$ref: "#/components/responses/Unauthorized"
"403":
description: |
The caller's account has `local_project_disabled=true`
(cannot index/reindex), or the caller can read the project but
is not its owner. Admins are exempt from the former.
content:
application/json:
schema:
$ref: "#/components/schemas/Error"
"404":
$ref: "#/components/responses/NotFound"
"409":
Expand Down Expand Up @@ -2907,7 +2924,7 @@ components:

User:
type: object
required: [id, email, role, must_change_password, created_at, updated_at, disabled]
required: [id, email, role, must_change_password, created_at, updated_at, disabled, local_project_disabled]
properties:
id:
type: string
Expand All @@ -2934,6 +2951,15 @@ components:
type: string
format: date-time
nullable: true
local_project_disabled:
type: boolean
description: |
When true, the user may not create local projects or
index/reindex any project (both return 403). Search of
already-indexed projects and workspace creation remain
allowed. Admins are always exempt regardless of this flag.
Defaults to false; existing users created before this field
was introduced are false (allowed) for backward compatibility.

UserWithStats:
allOf:
Expand Down Expand Up @@ -3051,6 +3077,12 @@ components:
description: |
When true, the user can no longer authenticate. Refused for
the last enabled admin when set to true.
local_project_disabled:
type: boolean
description: |
When true, the user may not create local projects or
index/reindex any project. Search and workspace creation stay
allowed; admins are always exempt. Has no last-admin guard.

RuntimeConfig:
type: object
Expand Down
37 changes: 29 additions & 8 deletions plugins/cix/agents/cix-workspace-investigator.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,11 +27,24 @@ of two shapes. Behave very differently depending on which:
nothing useful — there's nothing to read locally. The cix server has the
files, chunks, and symbols; you reach them only through the `cix` CLI.

**How to tell which you have:** run `cix list` once, then `grep` for the
exact identifier the main agent gave you.
**If the main agent gave you a server alias, use it on EVERY cix call.**
The `cix` CLI can have several named servers configured, and a workspace
(plus all its repos) lives on exactly one of them. The main agent will
tell you which — e.g. "this project is on server `corporate`". When it
does, add the global `--server <alias>` flag to *every* `cix` command
below, alongside `-n <project_name>`. Without it, cix talks to the
*default* server, where your assigned project doesn't exist, and every
call comes back empty (which looks like "nothing found" but is really
"wrong server"). If no alias was given, you're on the default server —
don't invent one.

**How to tell which shape your project is:** run `cix list` once (on the
right server), then `grep` for the exact identifier the main agent gave
you.

```bash
cix list | grep -F "<project identifier from main agent>"
cix list --server <alias> | grep -F "<project identifier from main agent>"
# (drop --server if the main agent didn't name one — default server)
```

- A line starting with `[✓] /` → local working tree.
Expand All @@ -47,6 +60,11 @@ the code.

You have a read-only toolkit for code investigation inside the assigned project:

> **Server flag.** If the main agent named a server (`--server <alias>`),
> append it to *every* command in this list, e.g.
> `cix search "<term>" -n <project_name> --server <alias>`. The examples
> below omit it for brevity; add it whenever you were given an alias.

- **`cix search "<term>" -n <project_name>`** — semantic / hybrid lookups
*inside the assigned project*. **Always pass `-n <project_name>`** (the
identifier from `cix list`); without it, cix searches whatever project
Expand All @@ -72,11 +90,14 @@ re-index.

## Hard rules — non-negotiable

1. **Stay inside the assigned project.** Every `cix` invocation MUST carry
`-n <project_name>`. Without it, cix searches the cwd's project — that's
the main session's repo, not yours. Don't read or query other workspace
repos. If a finding requires looking elsewhere, surface it as an
uncertainty for the main agent to fan out further.
1. **Stay inside the assigned project — and on the assigned server.**
Every `cix` invocation MUST carry `-n <project_name>`, plus
`--server <alias>` if the main agent named one. Without `-n`, cix
searches the cwd's project (the main session's repo, not yours);
without the right `--server`, it queries the wrong backend and returns
empty. Don't read or query other workspace repos. If a finding requires
looking elsewhere, surface it as an uncertainty for the main agent to
fan out further.
2. **Never hunt the filesystem for a remote-only project.** No
`find /`, no `locate`, no `ls -R ~`, no recursive Grep across `/`.
If `cix list` shows the project as `github.com/…@…`, the files do
Expand Down
84 changes: 73 additions & 11 deletions plugins/cix/skills/cix-workspace/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,36 @@ to implementation before you can answer all three with evidence.

---

## First: which server hosts the workspace?

The `cix` CLI can be configured with **several named servers** (a local
box, a remote corporate backend, …). Each server hosts its **own** set of
workspaces and projects — a workspace named `platform` on one server is
unrelated to anything on another. Every `cix` command targets the
**default** server unless you pass the global `--server <alias>` flag (or
set `CIX_SERVER`).

```bash
cix config show # lists configured servers; * marks the default
```

**A workspace and all its repos live on exactly one server.** So before
you run the workflow, decide which server you're on, then be **consistent**:
pass the *same* `--server <alias>` to *every* command in the flow —
`cix ws`, workspace search, per-project drill-down, and the sub-agent
fan-out. Mixing servers mid-workflow (orient on server A, drill down on
the default) silently returns empty or wrong-repo results, because the
project simply doesn't exist on the other server.

**Agent rule:** use the default server (no flag) unless the user names a
specific server, or the primary project's workspace isn't on the default.
Never guess an alias — run `cix config show` to see the configured names.
Once you know the alias, thread it through the whole workflow. The
examples below omit `--server` for readability; add it to **every** command
when the target workspace is on a non-default server.

---

## When to reach for workspace search

| Signal in the user's request | What to do |
Expand All @@ -57,10 +87,16 @@ The goal-driven loop. Don't shortcut it. Each step is fast.
### Step 0 — orient

```bash
cix ws # list workspaces; find the one your primary is in
cix config show # which servers exist? which is default?
cix ws # list workspaces on the (default) server
cix ws <name> # describe — confirm repos are indexed (✓ count)
```

If `cix ws` doesn't list the workspace your task is about, it may live on
a different server — re-run with `--server <alias>` (the alias from
`cix config show`) and keep that flag on every later command. Lock in the
server here, before searching.

If the workspace shows `stale_fts_repos` in any search response later,
trust the dense ranking less — see the troubleshooting section.

Expand Down Expand Up @@ -107,21 +143,28 @@ deeper read, not as the full answer.
For repos other than the primary, you have two options:

**A. Quick scan (≤ 2 repos to investigate):** use single-project
search directly.
search directly via the CLI, scoped with `-n` to the project. Pass the
`project_path` from the workspace-search `projects[]` panel verbatim:

```bash
# Search inside one specific project
curl -G -H "Authorization: Bearer $CIX_KEY" \
--data-urlencode "q=rate limit middleware handler" \
--data-urlencode "min_score=0" \
"$CIX_URL/api/v1/projects/$(project_hash)/search"
# Search inside one specific project (-n = exact project ID from cix list /
# the workspace projects[] panel). --server keeps it on the same backend
# the workspace lives on — REQUIRED when that's not the default server.
cix search "rate limit middleware handler" -n <project_path> --min-score 0
cix search "rate limit middleware handler" -n <project_path> --server corporate --min-score 0
```

The per-project default `min_score` is `0.2` — light floor that
keeps abstract NL queries non-empty. For drill-down on a natural-
language question ("how does X work end-to-end"), pass `min_score=0`
language question ("how does X work end-to-end"), pass `--min-score 0`
explicitly to be safe. For strict code-symbol matching, pass `0.4+`.

> Prefer the CLI over a raw `curl … /api/v1/projects/{hash}/search`: the
> CLI resolves the right server (and its key) from your config, so you
> can't accidentally point `$CIX_URL`/`$CIX_KEY` at a *different* server
> than the workspace. If you do hand-roll curl, make sure the URL + key
> belong to the server that hosts this workspace.

**B. Fan-out to sub-agents (≥ 3 repos, or you need a thorough read):**
spawn one `cix-workspace-investigator` sub-agent per relevant repo, in
parallel. See the dedicated [Sub-agent fan-out pattern](#sub-agent-fan-out-pattern)
Expand Down Expand Up @@ -366,6 +409,14 @@ entry in `projects[]`. Paste that string verbatim into the sub-agent prompt,
and tell the sub-agent explicitly which shape it is so it doesn't waste
calls grepping a tree that isn't there. One repo per spawn.

**Pass the server alias too.** If the workspace is on a non-default server,
the sub-agent's `cix search -n <project>` calls must carry the *same*
`--server <alias>` — otherwise they hit the default server, where the
project doesn't exist, and come back empty. State it plainly in the prompt:
"This project lives on server `corporate`; add `--server corporate` to every
`cix` call." If you're on the default server, say so (or omit it) so the
sub-agent doesn't invent a flag.

#### 3. Seed chunks **with your commentary**

This is the part most often done badly. Don't just paste raw chunk
Expand Down Expand Up @@ -404,6 +455,8 @@ Seed chunks from workspace search:
shared utilities.

Panel-level notes:
- Server: this project is on `corporate` — pass `--server corporate` on
every cix call (it does NOT exist on the default server).
- Workspace ranked this project #1 with a clear lead (project_score
1.000 vs next 0.860). High confidence this is the right repo.
- bm25_score=8.5, dense_score=0.54 — strong on both signals, not a
Expand Down Expand Up @@ -590,6 +643,11 @@ language to see what stack it actually uses, then refine.
## Quick command reference

```bash
# Servers (run first if more than one backend is configured)
cix config show # list servers; * marks the default
cix ws --server corporate # any command takes the global --server <alias>
# (CIX_SERVER=corporate also selects it; --server wins over the env var)

# List workspaces
cix ws
cix ws list --json
Expand Down Expand Up @@ -628,12 +686,16 @@ Flags:

When the user's task plausibly spans more than one repo:

0. `cix config show` → if more than one server, decide which one hosts
the workspace and thread the **same** `--server <alias>` through every
command below (default server → no flag needed).
1. `cix ws` → find the workspace, then `cix ws <name>` describe it.
2. Workspace search with a **short, term-rich** query.
3. Read `projects[]` → that's your scope (Q1 answered).
4. For each repo in scope, either single-project search or spawn a
`cix-workspace-investigator` sub-agent — in parallel, with seed
chunks AND your interpretive commentary on what to trust.
4. For each repo in scope, either single-project search (`cix search
-n <project_path>`) or spawn a `cix-workspace-investigator` sub-agent
— in parallel, with seed chunks, the server alias, AND your
interpretive commentary on what to trust.
5. Synthesize the sub-agent reports → plan changes per repo, with
order constraints (Q2 + Q3 answered).
6. Ask the user to confirm the scope and plan before implementing.
Expand Down
31 changes: 31 additions & 0 deletions server/dashboard/src/lib/cixServer.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
// Derive a cix CLI server alias from the browser host. The CLI stores each
// server as one entry under `server.<alias>` and parses that config key by
// splitting on dots, so the alias must be dot-free (and whitespace-free, and
// non-empty) — see validateServerName / parseServerKey in the CLI. We fold the
// host (including any non-default port, so distinct ports get distinct aliases)
// to [a-z0-9-]: "cix.example.com" -> "cix-example-com",
// "localhost:21847" -> "localhost-21847".
//
// Shared by the API-key "Connect the cix CLI" popup and the home-page
// onboarding card so the two never drift.
export function cixServerAlias(host: string): string {
const alias = host
.toLowerCase()
.replace(/[^a-z0-9]+/g, '-')
.replace(/^-+|-+$/g, '');
return alias || 'default';
}

// Build the one-paste command that registers a server (URL + key as a single
// `server.<alias>` entry) in the cix CLI config and makes it the default.
// Values are shell-safe (a URL, a `cix_<hex>` key, an [a-z0-9-] alias), so no
// quoting is needed — matching the CLI README's unquoted examples. `key`
// defaults to a `<key>` placeholder for previews where no secret is revealed.
export function cixConnectCommand(origin: string, host: string, key = '<key>'): string {
const alias = cixServerAlias(host);
return (
`cix config set server.${alias}.url ${origin} && ` +
`cix config set server.${alias}.key ${key} && ` +
`cix config set default_server ${alias}`
);
}
67 changes: 67 additions & 0 deletions server/dashboard/src/lib/useCopy.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
import { useCallback, useRef, useState } from 'react';
import { toast } from 'sonner';

// Last-resort copy when the async Clipboard API isn't available — happens on
// plain HTTP deploys (non-localhost) and inside some embedded webviews.
// document.execCommand('copy') is deprecated but universally implemented as of
// 2026; keeping it as a fallback turns "no way to copy" into "always works".
function legacyCopy(text: string): boolean {
if (typeof document === 'undefined') return false;
const ta = document.createElement('textarea');
ta.value = text;
ta.setAttribute('readonly', '');
ta.style.position = 'fixed';
ta.style.opacity = '0';
document.body.appendChild(ta);
ta.focus();
ta.select();
let ok = false;
try {
ok = document.execCommand('copy');
} catch {
ok = false;
}
document.body.removeChild(ta);
return ok;
}

// useCopy — one-click clipboard with a transient "copied" flag and graceful
// fallback. navigator.clipboard requires a secure context (HTTPS or localhost);
// on bare-IP / HTTP deploys it throws, so we fall back to document.execCommand
// through a transient textarea. On total failure we surface a toast telling the
// user to copy manually. `copied` flips true for `resetMs` after a success so
// callers can show a checkmark without re-implementing the timer.
export function useCopy(resetMs = 2000): {
copied: boolean;
copy: (text: string) => Promise<boolean>;
} {
const [copied, setCopied] = useState(false);
const timer = useRef<number | null>(null);

const copy = useCallback(
async (text: string): Promise<boolean> => {
let ok = false;
try {
if (window.isSecureContext && navigator.clipboard?.writeText) {
await navigator.clipboard.writeText(text);
ok = true;
} else {
ok = legacyCopy(text);
if (!ok) throw new Error('legacy copy failed');
}
} catch {
toast.error('Could not copy automatically.', {
description: 'Select the text, ⌘A / Ctrl-A, then copy.',
});
return false;
}
setCopied(true);
if (timer.current) window.clearTimeout(timer.current);
timer.current = window.setTimeout(() => setCopied(false), resetMs);
return ok;
},
[resetMs]
);

return { copied, copy };
}
Loading
Loading