Skip to content

feat(web): ui/ux cleanup#143

Merged
jamiepine merged 11 commits intospacedriveapp:mainfrom
skulldogged:ui-ux-improvements
Feb 25, 2026
Merged

feat(web): ui/ux cleanup#143
jamiepine merged 11 commits intospacedriveapp:mainfrom
skulldogged:ui-ux-improvements

Conversation

@skulldogged
Copy link
Contributor

Summary

Polishes the web UI across channels, skills, and overview pages, and fixes a few backend/API issues that were causing incorrect behavior in pagination and registry data.

Changes

  • Channels

    • Fixed branch/worker cards so collapsed text truncates correctly and expanded text wraps cleanly.
    • Improved branch/worker row alignment.
    • Fixed “load older messages” behavior (debounce + dedupe) and corrected backend timestamp pagination comparison.
  • Skills

    • Improved installed/browse card layout and alignment.
    • Updated installed-state action in browse cards (checkmark -> delete on hover/click).
    • Switched delete controls to neutral outline style.
    • Moved “Install from GitHub” section above the infinite list.
    • Added registry description support and surfaced total registry skill count.
  • Overview

    • Added/fixed edit links for routing, identity cards, memory, and configuration.
    • Improved activity heatmap/card sizing.
    • Updated memory donut card (title/center total) and fixed tooltip layering/tooltip sizing.
  • Shared UI

    • Standardized cortex chat toggle button styling across Channel, Memories, and Cortex pages (neutral style).
  • Backend

    • Fixed compile issues in browser tool URL parsing and API auth middleware extractor.
    • Added registry total passthrough and description enrichment fallback in skills API.

Testing

  • cargo check
  • npm run build

@bilawalriaz
Copy link
Contributor

I already have a PR for "Fixed compile issues in browser tool URL parsing and API auth middleware extractor" here

@coderabbitai
Copy link

coderabbitai bot commented Feb 24, 2026

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

Walkthrough

Adds optional registry fields (description, total), enables className on Markdown, deduplicates channel history merges, refactors several Agent/Channel UIs, implements cached on-demand GitHub SKILL.md enrichment, and changes SQL timestamp comparison to use datetime().

Changes

Cohort / File(s) Summary
API Interface Extensions
interface/src/api/client.ts
Added optional description?: string to RegistrySkill and total?: number to RegistryBrowseResponse.
Markdown Component
interface/src/components/Markdown.tsx
Added optional className prop; applied alongside base "markdown" class on container.
Channel Live State Hook
interface/src/hooks/useChannelLiveState.ts
Introduced itemKey(); changed history merge to deduplicate incoming timeline items and prepend only new items; derive hasMore from deduplication.
Agent Detail UI
interface/src/routes/AgentDetail.tsx
Refactored Memory/Model Routing/Configuration layouts to flex/grid; added per-item Edit links; replaced 7x24 heatmap with CSS grid; MemoryDonut shows total overlay; identity items gain per-tab edits and layout changes.
Agent Skills UI & State
interface/src/routes/AgentSkills.tsx
Updated InstalledSkill and RegistrySkillCard props/UI (added onRemove, isRemoving); switched installed tracking Set→Map; added GitHub install form; surfaced registry total; adjusted install/remove UX and icons.
Channel Detail UI & Loading
interface/src/routes/ChannelDetail.tsx
Adjusted branch/worker item truncation/wrapping; preserved whitespace in Markdown; added lastLoadMoreAtRef and 800ms throttle for load-more; refined cortex toggle layout.
Backend Skills Enrichment
src/api/skills.rs
Added description: Option<String> to RegistrySkill and total: Option<u64> to RegistryBrowseResponse. Implemented cached async enrichment: fetch SKILL.md from GitHub across branches/paths, parse short descriptions, cache results, and populate responses.
Data Layer Timestamp Handling
src/conversation/history.rs
Changed SQL filter to compare timestamps via datetime(timestamp) < datetime(?3) in load_channel_timeline.
Image Resolve Logic
src/update.rs
Loosened condition for splitting image into base and tag to allow tag parsing when there is no slash or when colon occurs after last slash.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

🚥 Pre-merge checks | ✅ 1 | ❌ 2

❌ Failed checks (1 warning, 1 inconclusive)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 62.96% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Title check ❓ Inconclusive The title 'feat(web): ui/ux cleanup' is vague and overly broad, using generic terms like 'cleanup' that don't convey specific information about the changeset's main improvements. Consider a more specific title that highlights the primary changes, such as 'feat(web): improve channel truncation, skills browsing, and agent overview layout' or similar.
✅ Passed checks (1 passed)
Check name Status Explanation
Description check ✅ Passed The description comprehensively relates to the changeset, detailing UI/UX improvements across channels, skills, overview pages, and backend fixes with organized sections matching the file changes.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
  • 📝 Generate docstrings (stacked PR)
  • 📝 Generate docstrings (commit on current branch)
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🧹 Nitpick comments (3)
interface/src/routes/ChannelDetail.tsx (1)

321-331: Minor indentation inconsistency.

The cortex toggle wrapper <div> at line 321 appears to break from the surrounding indentation level (it's inside the <div className="ml-auto flex items-center gap-3"> but doesn't share its indent). Not a functional issue, but it makes the JSX structure harder to read at a glance.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@interface/src/routes/ChannelDetail.tsx` around lines 321 - 331, Indentation
of the cortex toggle wrapper div in ChannelDetail.tsx is inconsistent; adjust
the JSX so the <div className="flex overflow-hidden rounded-md border
border-app-line bg-app-darkBox"> (the cortex toggle wrapper containing the
Button that calls setCortexOpen and reads cortexOpen) is indented to match the
surrounding <div className="ml-auto flex items-center gap-3"> children, keeping
the Button and its props (onClick, variant, size, className, title) aligned with
the other sibling elements for consistent JSX formatting.
src/api/skills.rs (2)

256-263: Description enrichment adds latency to every browse/search response.

enrich_registry_descriptions runs inline in both registry_browse and registry_search handlers, blocking the response until all GitHub fetches complete (up to 3s timeout × 4 candidate paths per skill, though parallelized). First-time page loads with many uncached skills will be noticeably slow.

Consider enriching descriptions asynchronously (fire-and-forget background task that populates the cache) and returning whatever is cached, or returning partial results and letting the frontend re-fetch.

Also applies to: 306-313

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/api/skills.rs` around lines 256 - 263, The call to
enrich_registry_descriptions is blocking the HTTP response in registry_browse
and registry_search by awaiting GitHub fetches; change to fire-and-forget
background enrichment so the handlers return immediately with whatever
descriptions are already cached. Concretely, replace the await
enrich_registry_descriptions(&client, &mut skills).await with spawning a
background task (e.g., tokio::spawn or equivalent) that clones the needed
client/config and the list of skill identifiers and calls
enrich_registry_descriptions in the background to populate the cache, while the
handler returns Json(RegistryBrowseResponse { skills, ... }) (and do the same
for the RegistrySearchResponse branch). Ensure any shared cache or client is
cloned or Arc-wrapped and handle errors inside the task so the handler never
awaits it.

12-13: Unbounded in-memory cache will grow indefinitely.

REGISTRY_SKILL_DESCRIPTION_CACHE has no eviction policy, TTL, or size limit. Over time, as users browse/search different skills, this HashMap grows without bound. With thousands of registry skills, each storing a description string up to 220 chars, the memory impact is modest per entry but there's no ceiling.

Consider adding a bounded cache (e.g., an LRU with a max capacity) or a simple TTL-based eviction. Even a periodic clear on a timer would suffice.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/api/skills.rs` around lines 12 - 13, REGISTRY_SKILL_DESCRIPTION_CACHE is
an unbounded LazyLock<HashMap<String,String>> and will grow indefinitely;
replace it with a bounded, concurrent cache (e.g., an LRU or TTL cache) or add
periodic eviction: for example swap the LazyLock<RwLock<HashMap<...>>> for a
concurrent cache type (moka::sync::Cache or lru::LruCache behind a
tokio::sync::Mutex) with a configured max_capacity and/or TTL, update all
accesses that read/write REGISTRY_SKILL_DESCRIPTION_CACHE to use the cache's
get/insert/evict APIs (or implement a background task that periodically prunes
old entries if you prefer TTL), and ensure the chosen cache provides safe
async/concurrent usage in place of the current tokio::sync::RwLock wrapper.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@interface/src/routes/AgentSkills.tsx`:
- Around line 353-411: The GitHub form is sharing the global installMutation,
causing its UI states and messages to reflect installs triggered from registry
cards; create a separate mutation for the GitHub form (e.g.,
githubInstallMutation using the same useMutation handler currently used by
installMutation) and replace usages inside the form: onSubmit should call
githubInstallMutation.mutate(spec), the submit button disabled/isPending checks
should use githubInstallMutation.isPending, and the success/error messages
should use githubInstallMutation.isSuccess / githubInstallMutation.isError and
githubInstallMutation.data; leave the existing installMutation in place for
registry card buttons so their feedback remains scoped to registry installs.

In `@src/api/skills.rs`:
- Around line 393-394: The code currently builds raw URLs with a hardcoded
"main" branch (in the loop iterating candidate_paths and building url from
source and path), which fails for repos whose default branch is not main; change
the construction in that loop to resolve the default branch instead — either
call the GitHub REST API (GET /repos/{owner}/{repo}) to read default_branch and
use it when formatting url, or use the raw.githubusercontent.com "HEAD" redirect
by formatting url as "https://raw.githubusercontent.com/{source}/HEAD/{path}" so
the request follows the repo's default branch, and if you prefer robustness add
a small fallback sequence (try HEAD, then "main", then "master") when fetching
each candidate path.
- Around line 316-363: enrich_registry_descriptions is not caching "not found"
results so skills without SKILL.md trigger repeated HTTP calls; change the logic
to store a sentinel (e.g., an empty string or a dedicated marker) in
REGISTRY_SKILL_DESCRIPTION_CACHE when fetch_registry_skill_description returns
None, and when reading from REGISTRY_SKILL_DESCRIPTION_CACHE treat that sentinel
as meaning "looked up but no description" (i.e., set skills[index].description =
None/skip) so failed lookups aren't retried; update the join_set result handling
around fetch_registry_skill_description, the cache.insert call, and the initial
cached read to recognize and handle the sentinel (use registry_skill_key,
REGISTRY_SKILL_DESCRIPTION_CACHE, enrich_registry_descriptions, and
fetch_registry_skill_description to locate changes).

---

Nitpick comments:
In `@interface/src/routes/ChannelDetail.tsx`:
- Around line 321-331: Indentation of the cortex toggle wrapper div in
ChannelDetail.tsx is inconsistent; adjust the JSX so the <div className="flex
overflow-hidden rounded-md border border-app-line bg-app-darkBox"> (the cortex
toggle wrapper containing the Button that calls setCortexOpen and reads
cortexOpen) is indented to match the surrounding <div className="ml-auto flex
items-center gap-3"> children, keeping the Button and its props (onClick,
variant, size, className, title) aligned with the other sibling elements for
consistent JSX formatting.

In `@src/api/skills.rs`:
- Around line 256-263: The call to enrich_registry_descriptions is blocking the
HTTP response in registry_browse and registry_search by awaiting GitHub fetches;
change to fire-and-forget background enrichment so the handlers return
immediately with whatever descriptions are already cached. Concretely, replace
the await enrich_registry_descriptions(&client, &mut skills).await with spawning
a background task (e.g., tokio::spawn or equivalent) that clones the needed
client/config and the list of skill identifiers and calls
enrich_registry_descriptions in the background to populate the cache, while the
handler returns Json(RegistryBrowseResponse { skills, ... }) (and do the same
for the RegistrySearchResponse branch). Ensure any shared cache or client is
cloned or Arc-wrapped and handle errors inside the task so the handler never
awaits it.
- Around line 12-13: REGISTRY_SKILL_DESCRIPTION_CACHE is an unbounded
LazyLock<HashMap<String,String>> and will grow indefinitely; replace it with a
bounded, concurrent cache (e.g., an LRU or TTL cache) or add periodic eviction:
for example swap the LazyLock<RwLock<HashMap<...>>> for a concurrent cache type
(moka::sync::Cache or lru::LruCache behind a tokio::sync::Mutex) with a
configured max_capacity and/or TTL, update all accesses that read/write
REGISTRY_SKILL_DESCRIPTION_CACHE to use the cache's get/insert/evict APIs (or
implement a background task that periodically prunes old entries if you prefer
TTL), and ensure the chosen cache provides safe async/concurrent usage in place
of the current tokio::sync::RwLock wrapper.

ℹ️ Review info

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 7170ca0 and 66c9351.

📒 Files selected for processing (8)
  • interface/src/api/client.ts
  • interface/src/components/Markdown.tsx
  • interface/src/hooks/useChannelLiveState.ts
  • interface/src/routes/AgentDetail.tsx
  • interface/src/routes/AgentSkills.tsx
  • interface/src/routes/ChannelDetail.tsx
  • src/api/skills.rs
  • src/conversation/history.rs

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
interface/src/routes/ChannelDetail.tsx (1)

153-183: ⚠️ Potential issue | 🔴 Critical

Fix WorkerRunItem JSX + missing expansion state.
expanded is referenced without state and the Link is closed with </button>, which will fail to compile and breaks expansion behavior. Add local state, a toggle, and close the Link correctly (or restructure the wrapper to avoid nested interactive elements).

🛠️ Suggested fix (adds state + toggle and corrects JSX structure)
 function WorkerRunItem({ item, agentId }: { item: TimelineWorkerRun; agentId: string }) {
+	const [expanded, setExpanded] = useState(false);
 	return (
 		<div className="flex gap-3 px-3 py-2">
 			<span className="flex-shrink-0 pt-0.5 text-tiny text-ink-faint">
 				{formatTimestamp(new Date(item.started_at).getTime())}
 			</span>
 			<div className="min-w-0 flex-1">
-				<Link
-					to="/agents/$agentId/workers"
-					params={{ agentId }}
-					search={{ worker: item.id }}
-					className="block w-full rounded-md bg-amber-500/10 px-3 py-2 text-left transition-colors hover:bg-amber-500/15"
-				>
-					<div className="flex min-w-0 items-center gap-2 overflow-hidden">
-						<div className="h-2 w-2 rounded-full bg-amber-400/50" />
-						<span className="text-sm font-medium text-amber-300">Worker</span>
-						<span className="min-w-0 flex-1 truncate text-sm text-ink-dull">{item.task}</span>
-						{item.result && (
-							<span className="flex-shrink-0 self-start text-tiny leading-5 text-ink-faint">
-								{expanded ? "▾" : "▸"}
-							</span>
-						)}
-					</div>
-				</button>
+				<div className="rounded-md bg-amber-500/10 px-3 py-2 text-left transition-colors hover:bg-amber-500/15">
+					<div className="flex min-w-0 items-center gap-2 overflow-hidden">
+						<div className="h-2 w-2 rounded-full bg-amber-400/50" />
+						<span className="text-sm font-medium text-amber-300">Worker</span>
+						<Link
+							to="/agents/$agentId/workers"
+							params={{ agentId }}
+							search={{ worker: item.id }}
+							className="min-w-0 flex-1 truncate text-sm text-ink-dull hover:text-ink"
+						>
+							{item.task}
+						</Link>
+						{item.result && (
+							<button
+								type="button"
+								aria-expanded={expanded}
+								onClick={(e) => {
+									e.stopPropagation();
+									setExpanded(!expanded);
+								}}
+								className="flex-shrink-0 self-start text-tiny leading-5 text-ink-faint"
+							>
+								{expanded ? "▾" : "▸"}
+							</button>
+						)}
+					</div>
+				</div>
 				{expanded && item.result && (
 					<div className="mt-1 rounded-md border border-amber-500/10 bg-amber-500/5 px-3 py-2">
 						<div className="text-sm text-ink-dull">
 							<Markdown className="whitespace-pre-wrap break-words">{item.result}</Markdown>
 						</div>
 					</div>
 				)}
 			</div>
 		</div>
 	);
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@interface/src/routes/ChannelDetail.tsx` around lines 153 - 183, The
WorkerRunItem component references expanded without state and incorrectly closes
the Link with </button>, breaking compilation and expansion behavior; add a
local boolean state (e.g., const [expanded, setExpanded] = useState(false>)
inside WorkerRunItem, add a toggle handler (setExpanded(prev => !prev)) wired to
a non-conflicting control (e.g., an onClick on a chevron span or a dedicated
button) and ensure the Link JSX is closed with </Link> (or restructure to avoid
nesting interactive elements) so the expansion conditional (expanded &&
item.result) works and the markup is valid.
♻️ Duplicate comments (1)
src/api/skills.rs (1)

394-405: Negative caching is now correctly implemented — previous concern addressed.

description (Option<String>) is always inserted into REGISTRY_SKILL_DESCRIPTION_CACHE, including None for skills without a SKILL.md. Both read-sites correctly distinguish a cache miss (getNone) from a cached negative (getSome(None)), preventing repeated HTTP storms for missing descriptions.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/api/skills.rs` around lines 394 - 405, Negative-caching is intentionally
used here: the Option<String> named description (including None when SKILL.md is
missing) is always inserted into REGISTRY_SKILL_DESCRIPTION_CACHE, but readers
distinguish a cache miss (None) from a cached negative (Some(None)). Add a brief
clarifying comment immediately above the
REGISTRY_SKILL_DESCRIPTION_CACHE.insert(cache_key, description) call (near
join_set.join_next handling and skills.get_mut usage) stating that inserting
None is intentional for negative caching and that read-sites must check for
Some(None) vs None; no logic change required beyond the comment.
🧹 Nitpick comments (1)
interface/src/routes/AgentSkills.tsx (1)

233-255: githubInstallMutation and installMutation share an identical mutationFn and onSuccess.

Both were kept separate to isolate UI state, which is correct. But the implementation is duplicated. Extract a shared function to keep them DRY:

♻️ Proposed refactor
+    const skillInstallFn = (spec: string) =>
+        api.installSkill({ agent_id: agentId, spec, instance: false });
+    const onInstallSuccess = () =>
+        queryClient.invalidateQueries({ queryKey: ["skills", agentId] });
+
     const installMutation = useMutation({
-        mutationFn: (spec: string) =>
-            api.installSkill({ agent_id: agentId, spec, instance: false }),
-        onSuccess: () => {
-            queryClient.invalidateQueries({ queryKey: ["skills", agentId] });
-        },
+        mutationFn: skillInstallFn,
+        onSuccess: onInstallSuccess,
     });

     const githubInstallMutation = useMutation({
-        mutationFn: (spec: string) =>
-            api.installSkill({ agent_id: agentId, spec, instance: false }),
-        onSuccess: () => {
-            queryClient.invalidateQueries({ queryKey: ["skills", agentId] });
-        },
+        mutationFn: skillInstallFn,
+        onSuccess: onInstallSuccess,
     });
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@interface/src/routes/AgentSkills.tsx` around lines 233 - 255, installMutation
and githubInstallMutation duplicate the same mutationFn and onSuccess; extract a
shared helper (e.g., createInstallMutationOptions or getInstallMutationHandlers)
that returns the mutationFn (calling api.installSkill with agent_id: agentId,
spec, instance: false) and the onSuccess callback (calling
queryClient.invalidateQueries({ queryKey: ["skills", agentId] })), then use that
helper when creating both installMutation and githubInstallMutation to remove
the duplicated implementation while preserving separate useMutation instances
for UI state.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@interface/src/hooks/useChannelLiveState.ts`:
- Around line 586-594: The current code sets hasMore to false when deduplication
yields zero new items (olderItems.length === 0) which can prematurely stop
pagination; change the logic so hasMore reflects the API's data.has_more (do not
override it based on olderItems length), and implement/propagate a cursor from
the response (e.g., response cursor/last_cursor) into the channel state for
subsequent fetches—update the merge block for [channelId] (where existingKeys,
olderItems, itemKey, data.has_more, timeline are used) to store the returned
cursor and use data.has_more directly to decide hasMore so pagination continues
even when dedupe returns no new items.

In `@src/api/skills.rs`:
- Around line 473-480: The nested conditional in the for loop triggers Clippy's
`collapsible_if`; change the logic to an early-continue pattern: inside the for
(index, line) in lines.iter().enumerate() loop, replace the nested if that
checks heading.starts_with('#') && heading.contains("description") with a single
guard that continues when the condition is false, then call
extract_paragraph(&lines[(index + 1)..]) and return Some(description) when it
yields Some. This keeps the same behavior while removing the nested if and
references the existing symbols heading, lines, index, and the
extract_paragraph(...) call.

---

Outside diff comments:
In `@interface/src/routes/ChannelDetail.tsx`:
- Around line 153-183: The WorkerRunItem component references expanded without
state and incorrectly closes the Link with </button>, breaking compilation and
expansion behavior; add a local boolean state (e.g., const [expanded,
setExpanded] = useState(false>) inside WorkerRunItem, add a toggle handler
(setExpanded(prev => !prev)) wired to a non-conflicting control (e.g., an
onClick on a chevron span or a dedicated button) and ensure the Link JSX is
closed with </Link> (or restructure to avoid nesting interactive elements) so
the expansion conditional (expanded && item.result) works and the markup is
valid.

---

Duplicate comments:
In `@src/api/skills.rs`:
- Around line 394-405: Negative-caching is intentionally used here: the
Option<String> named description (including None when SKILL.md is missing) is
always inserted into REGISTRY_SKILL_DESCRIPTION_CACHE, but readers distinguish a
cache miss (None) from a cached negative (Some(None)). Add a brief clarifying
comment immediately above the REGISTRY_SKILL_DESCRIPTION_CACHE.insert(cache_key,
description) call (near join_set.join_next handling and skills.get_mut usage)
stating that inserting None is intentional for negative caching and that
read-sites must check for Some(None) vs None; no logic change required beyond
the comment.

---

Nitpick comments:
In `@interface/src/routes/AgentSkills.tsx`:
- Around line 233-255: installMutation and githubInstallMutation duplicate the
same mutationFn and onSuccess; extract a shared helper (e.g.,
createInstallMutationOptions or getInstallMutationHandlers) that returns the
mutationFn (calling api.installSkill with agent_id: agentId, spec, instance:
false) and the onSuccess callback (calling queryClient.invalidateQueries({
queryKey: ["skills", agentId] })), then use that helper when creating both
installMutation and githubInstallMutation to remove the duplicated
implementation while preserving separate useMutation instances for UI state.

ℹ️ Review info

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 66c9351 and be6c800.

⛔ Files ignored due to path filters (2)
  • Cargo.lock is excluded by !**/*.lock, !**/*.lock
  • Cargo.toml is excluded by !**/*.toml
📒 Files selected for processing (6)
  • interface/src/api/client.ts
  • interface/src/hooks/useChannelLiveState.ts
  • interface/src/routes/AgentSkills.tsx
  • interface/src/routes/ChannelDetail.tsx
  • src/api/skills.rs
  • src/conversation/history.rs
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/conversation/history.rs

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/api/skills.rs`:
- Around line 394-397: The loop calling join_set.join_next() currently discards
task failures by matching only Ok(...) and continuing on anything else; update
the while let Some(result) = join_set.join_next().await loop to explicitly
handle the Err case (and non-Ok variants) instead of silently continuing:
inspect the Result from join_next(), log the error with context (include
index/cache_key when available) or propagate it up (return Err) so enrichment
failures are visible, and only continue on expected success; change the pattern
matching around the let Ok((index, cache_key, description)) = result else {
continue; } to handle and report the error path for debugging and correctness.
- Around line 438-456: The code in the HTTP fetch loop (using
client.get(&url)...send().await and response.text().await) currently swallows
errors (Err(_) branches) and skips non-success statuses without any logging;
update those branches to log diagnostic details (at debug/trace level) including
the URL, the error returned from the request or text() call, and the
response.status() for non-success cases so failures are observable; locate the
match arms around client.get(...).send().await and the match for
response.text().await in skills.rs and replace the Err(_) and the
!response.status().is_success() paths with calls to your project's logging
facility (e.g., debug!/trace!/error!) that include the url, error, and status to
aid debugging.

ℹ️ Review info

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between be6c800 and 066eaea.

📒 Files selected for processing (1)
  • src/api/skills.rs

jamiepine and others added 2 commits February 24, 2026 20:20
- Updated WorkerRunItem to simplify the rendering logic by removing unnecessary conditional rendering for the result indicator.
- Changed the button to a Link component for improved navigation.
- Enhanced the display of the result section to ensure it is always shown when available.
@jamiepine jamiepine enabled auto-merge February 25, 2026 04:27
@jamiepine jamiepine merged commit d47cfb9 into spacedriveapp:main Feb 25, 2026
3 of 4 checks passed
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
interface/src/routes/ChannelDetail.tsx (1)

118-123: ⚠️ Potential issue | 🟡 Minor

Add ARIA state to the expandable Branch section.

This toggle is interactive but doesn’t expose expanded/collapsed state to assistive tech. Add aria-expanded and aria-controls on the button, and an id on the controlled panel.

Suggested accessibility fix
 function BranchRunItem({ item }: { item: TimelineBranchRun }) {
 	const [expanded, setExpanded] = useState(false);
+	const conclusionId = `branch-conclusion-${item.id}`;

 	return (
 		<div className="flex gap-3 px-3 py-2">
@@
 				<Button
 					type="button"
 					onClick={() => setExpanded(!expanded)}
+					aria-expanded={expanded}
+					aria-controls={item.conclusion ? conclusionId : undefined}
 					variant="ghost"
 					className="h-auto w-full justify-start rounded-md bg-violet-500/10 px-3 py-2 text-left hover:bg-violet-500/15"
 				>
@@
 				{expanded && item.conclusion && (
-					<div className="mt-1 rounded-md border border-violet-500/10 bg-violet-500/5 px-3 py-2">
+					<div id={conclusionId} className="mt-1 rounded-md border border-violet-500/10 bg-violet-500/5 px-3 py-2">
 						<div className="text-sm text-ink-dull">
 							<Markdown className="whitespace-pre-wrap break-words">{item.conclusion}</Markdown>
 						</div>
 					</div>
 				)}

Also applies to: 141-145

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@interface/src/routes/ChannelDetail.tsx` around lines 118 - 123, The Branch
toggle Button (the element using onClick={() => setExpanded(!expanded)} and the
expanded state variable) lacks ARIA state: add aria-expanded={expanded} and
aria-controls="<unique-panel-id>" to that Button and ensure the controlled panel
element (the collapsible panel rendered when expanded is true) has
id="<unique-panel-id>" (use a unique descriptive id like
"branch-panel-<channelId>" or similar) so assistive tech can read the
expanded/collapsed state; apply the same changes to the second Button instance
referenced around the other section (the one at lines 141-145) and ensure both
aria-controls values match their respective panel id attributes.
src/update.rs (1)

584-590: ⚠️ Potential issue | 🔴 Critical

Declare rust-version = "1.82" in Cargo.toml — Option::is_none_or requires it.

The code uses Option::is_none_or, which was stabilized in Rust 1.82.0 (October 2024). The project does not declare an explicit rust-version in Cargo.toml, meaning it has no MSRV floor. This will cause compilation failures on Rust < 1.82.0.

Either:

  1. Add rust-version = "1.82" to the [package] section in Cargo.toml, or
  2. Rewrite the guard to use last_slash.is_none() || last_colon.map_or(false, |colon| last_slash.is_none_or(|slash| colon > slash)) for compatibility with older Rust versions.

The logic itself is correct: is_none_or(|slash| colon > slash) properly handles bare image names without a registry prefix (e.g., spacebot:slim). However, consider adding a test case for this scenario (bare name + explicit tag) to the test suite, as all current test cases include a registry prefix.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/update.rs` around lines 584 - 590, The code uses Option::is_none_or in
src/update.rs around the tuple extraction for (base, tag) using last_colon,
last_slash and image_without_digest; since is_none_or needs Rust 1.82+, either
add rust-version = "1.82" to the [package] section of Cargo.toml to set the
MSRV, or replace the guard expression (the match arm that checks Some(colon) if
last_slash.is_none_or(|slash| colon > slash) ) with an equivalent pre-1.82
construct that first checks last_slash.is_none() and otherwise compares
positions (e.g., use last_slash.is_none() || last_slash.map_or(false, |slash|
colon > slash) applied together with last_colon.map_or), keeping the same
behavior for image_without_digest, last_colon and last_slash; also add a unit
test covering a bare image name with an explicit tag (e.g., "spacebot:slim") to
ensure the branch selecting the tag works.
🧹 Nitpick comments (1)
src/update.rs (1)

638-704: Add a test covering the primary case this fix addresses: bare image name with an explicit tag.

The existing suite tests registry-prefixed paths and registry.local:5000/spacebot (no tag), but none of the cases exercise a bare image name with an explicit tag — which is the exact scenario is_none_or was introduced to fix. Without a test, a future regression to map_or(false, ...) would go undetected.

🧪 Suggested additional test cases
+        // Bare image name with explicit variant tag — the primary case fixed by is_none_or
+        assert_eq!(
+            resolve_target_image("spacebot:slim", "0.2.0", Some(ImageVariant::Full)),
+            "spacebot:v0.2.0-slim"
+        );
+        assert_eq!(
+            resolve_target_image("spacebot:latest", "0.2.0", Some(ImageVariant::Full)),
+            "spacebot:v0.2.0-full"
+        );
+        // Bare image name with no tag at all
+        assert_eq!(
+            resolve_target_image("spacebot", "0.2.0", Some(ImageVariant::Full)),
+            "spacebot:v0.2.0-full"
+        );
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/update.rs` around lines 638 - 704, Add a test in
test_resolve_target_image that covers a bare image name with an explicit tag
(e.g., "spacebot:v0.1.0") to ensure resolve_target_image handles non-registry
names with tags; call resolve_target_image with the existing new_version
("0.2.0") and both ImageVariant::Full and ImageVariant::Slim and assert the
returned strings become "spacebot:v0.2.0-full" (or "-slim" for the slim
variant), so regressions reverting the is_none_or behavior are caught.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@interface/src/routes/ChannelDetail.tsx`:
- Around line 324-330: The Cortex toggle button only uses title for
accessibility; update the Button element (the one using setCortexOpen and
cortexOpen) to include explicit accessibility attributes: add aria-label="Toggle
cortex chat" (or a more descriptive label) and aria-pressed={cortexOpen} so
assistive tech receives the button purpose and current state; keep the existing
title and visual styling but ensure aria-pressed is a boolean tied to cortexOpen
and aria-label matches the action.

---

Outside diff comments:
In `@interface/src/routes/ChannelDetail.tsx`:
- Around line 118-123: The Branch toggle Button (the element using onClick={()
=> setExpanded(!expanded)} and the expanded state variable) lacks ARIA state:
add aria-expanded={expanded} and aria-controls="<unique-panel-id>" to that
Button and ensure the controlled panel element (the collapsible panel rendered
when expanded is true) has id="<unique-panel-id>" (use a unique descriptive id
like "branch-panel-<channelId>" or similar) so assistive tech can read the
expanded/collapsed state; apply the same changes to the second Button instance
referenced around the other section (the one at lines 141-145) and ensure both
aria-controls values match their respective panel id attributes.

In `@src/update.rs`:
- Around line 584-590: The code uses Option::is_none_or in src/update.rs around
the tuple extraction for (base, tag) using last_colon, last_slash and
image_without_digest; since is_none_or needs Rust 1.82+, either add rust-version
= "1.82" to the [package] section of Cargo.toml to set the MSRV, or replace the
guard expression (the match arm that checks Some(colon) if
last_slash.is_none_or(|slash| colon > slash) ) with an equivalent pre-1.82
construct that first checks last_slash.is_none() and otherwise compares
positions (e.g., use last_slash.is_none() || last_slash.map_or(false, |slash|
colon > slash) applied together with last_colon.map_or), keeping the same
behavior for image_without_digest, last_colon and last_slash; also add a unit
test covering a bare image name with an explicit tag (e.g., "spacebot:slim") to
ensure the branch selecting the tag works.

---

Nitpick comments:
In `@src/update.rs`:
- Around line 638-704: Add a test in test_resolve_target_image that covers a
bare image name with an explicit tag (e.g., "spacebot:v0.1.0") to ensure
resolve_target_image handles non-registry names with tags; call
resolve_target_image with the existing new_version ("0.2.0") and both
ImageVariant::Full and ImageVariant::Slim and assert the returned strings become
"spacebot:v0.2.0-full" (or "-slim" for the slim variant), so regressions
reverting the is_none_or behavior are caught.

ℹ️ Review info

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 5669caa and 4d5d751.

📒 Files selected for processing (2)
  • interface/src/routes/ChannelDetail.tsx
  • src/update.rs

Comment on lines +324 to +330
<Button
onClick={() => setCortexOpen(!cortexOpen)}
variant={cortexOpen ? "secondary" : "ghost"}
size="icon"
className={cortexOpen ? "bg-app-selected text-ink" : ""}
title="Toggle cortex chat"
>
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Add accessible labeling/state to the icon-only Cortex toggle.

title alone is not enough for robust accessibility. Add explicit aria-label and aria-pressed.

Suggested accessibility fix
 							<Button
 								onClick={() => setCortexOpen(!cortexOpen)}
 								variant={cortexOpen ? "secondary" : "ghost"}
 								size="icon"
 								className={cortexOpen ? "bg-app-selected text-ink" : ""}
+								aria-label={cortexOpen ? "Close cortex chat" : "Open cortex chat"}
+								aria-pressed={cortexOpen}
 								title="Toggle cortex chat"
 							>
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@interface/src/routes/ChannelDetail.tsx` around lines 324 - 330, The Cortex
toggle button only uses title for accessibility; update the Button element (the
one using setCortexOpen and cortexOpen) to include explicit accessibility
attributes: add aria-label="Toggle cortex chat" (or a more descriptive label)
and aria-pressed={cortexOpen} so assistive tech receives the button purpose and
current state; keep the existing title and visual styling but ensure
aria-pressed is a boolean tied to cortexOpen and aria-label matches the action.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants