Skip to content

feat(skill): add atomic skill folder import#68

Open
ObsisMc wants to merge 28 commits into
ora-space:mainfrom
ObsisMc:skill-upload
Open

feat(skill): add atomic skill folder import#68
ObsisMc wants to merge 28 commits into
ora-space:mainfrom
ObsisMc:skill-upload

Conversation

@ObsisMc

@ObsisMc ObsisMc commented Jul 18, 2026

Copy link
Copy Markdown
Contributor

Add POST /api/skills/import to upload a local skill folder as one multipart request and commit it atomically across the filesystem and the database.

Files stream into an atoms/skills/<uuid>.tmp staging directory, the staged SKILL.md front matter resolves the skill name and description, then a unit of work inserts the row, renames staging to the committed <name> directory, and commits together — so a failure at any point leaves neither a row nor a promoted directory. Startup reconciliation discards *.tmp staging and any committed directory not backed by a visible skill row, and skill deletion now removes its committed folder.

Keep the orchestration hexagonal: a SkillPackageStore port with a local filesystem adapter, a SkillImportUnitOfWork closure port whose SQLite adapter holds the transaction open across the directory promote, and folder/manifest validation, all under crates/application/src/skill. Guard uploads with a 50MB body limit, a 1000-file cap, safe-name and safe-path checks, and empty/duplicate-path rejection, mapping failures to 422/409/413 responses.

Re1lya and others added 28 commits July 21, 2026 12:28
Replaces the add item inside each tree row's overflow menu with a
hover-revealed plus button, so creating a session against a project
or worktree is one click instead of two.
Selecting a row re-expanded its ancestors, so clicking an expanded row
collapsed it and immediately reopened it in the same tick. Only the second
click appeared to do anything, because by then the selection no longer
changed and the effect stayed quiet.

The effect existed so a freshly created task or session is revealed, which
is the creating mutation's concern rather than the selection's. Move it to
useCreateTask and useCreateSession, leaving nothing that reacts to
selection changes, so a row click is free to collapse what it just
selected.
Wrap each level of the tree in the shared Collapsible so a branch slides
instead of snapping. Height comes from the pixel value Base UI measures
into --collapsible-panel-height, following the pattern the shared Accordion
already uses: this sidebar ships to both the desktop shell and the browser,
so it runs on WebKit either way, where animating a 0fr/1fr grid track is
markedly less dependable.

Note the panel cannot borrow tw-animate-css's animate-collapsible-*
classes. Their keyframes read Radix/Bits/Reka/Kobalte height variables,
none of which Base UI sets, so they would fall back to height: auto and
never animate at all.
  Move execution context above the composer
and simplify the input surface.
  Group project, branch, and environment selectors
with clearer dropdown affordances, responsive truncation,
compact menus, and concise labels.
Add coverage for the updated context placement.
Port the turn-scoped chat UI (streamed thoughts, execution plans, tool
lifecycle, grouped tool activity, and diffs) onto the real streaming
backend. The prior store only kept text chunks and discarded every other
ACP session update, so tool calls and thinking never reached the UI.

Rebuild the chat store around the turn model while keeping the existing
async-iterator client interface and public method signatures unchanged:
sendMessage seeds the user turn and folds each agent update into it, and
loadSession reconstructs turns from replayed history by splitting on user
message chunks. Permission and cancellation handling are preserved.

Wire message-list, chat-view, message-bubble, and workspace-view to the
turn model, add the rendering components, i18n keys, and the diff
dependency. No backend contract changes.
Wire the ModelSelector into the composer footer's right side next to the
send button and drop the send-hint text. Add the chat.modelSelector.label
i18n key and remove the now-unused chat.sendHint.
…activity

The sidebar showed the animated "working" dots whenever Session.status was
"running", but that status is a backend-owned process-lifecycle flag: it stays
"running" for the whole life of the agent process, through every idle gap
between turns. So an alive-but-idle session animated forever.

Read the live signal instead - conversations[session.id].isResponding from the
chat store, which is true only while a prompt turn is actually streaming. This
restores the intent of the original "drive session status from live ACP prompt
activity" change as frontend-only logic: the REST contract no longer exposes a
session status write (no session.update), so the indicator derives from the
store at render time rather than round-tripping through the backend. Computing
it at render also keeps it correct across the session-list refetches that would
otherwise clobber a cached override.
When a session finishes a turn while the user is looking at a different one,
nothing signalled that something new landed - the running animation simply
disappeared and the row went quiet. Add a purely client-side unread mark that
fills the same icon slot the activity dots vacate.

A small zustand ledger (unread-sessions-store) holds the flag and never leaves
the browser; the backend has no read state. A sync hook mounted on the shell
watches the chat store for a turn ending (isResponding true -> false) while the
session is not the active selection, and clears the flag the moment the session
is opened. The sidebar renders a single filled primary dot - the shell's accent
for "new", distinct from emerald (running) and amber (permission) - with the
running animation still taking priority so a row never shows both.
  Add a responsive, keyboard-accessible path picker for
selecting files and directories in the web client.
Paint minimize/maximize/close controls and a draggable title bar for
frameless Windows/Linux windows via a new platform windowControls
capability; macOS and Web report none. Center the window on launch and
enlarge the default size (1280x832, min 900x600).

Refine the workspace header (session name over project/task, drop the
redundant new-task title and button) and bump the sidebar, headers, and
user profile typography/icons by ~15% while leaving the chat view.
  Add safe GitHub-flavored Markdown rendering
for assistant messages.
  Use Shiki themes for syntax highlighting and provide
localized copy, collapse, and line-count controls for
fenced code blocks.
  Improve multilingual typography and native font
rendering across desktop themes.
Add a permission selector to the composer footer's left edge, mirroring
the approval policy from settings. Full access ("trusted") is the only
selectable policy for now and is tinted amber; the stricter policies are
shown but disabled. Default the approval policy to trusted so the pill
reads as full access.

Also simplify the footer: replace the attachment button with an inert
plus placeholder, drop the agent/chat mode dropdown, and remove the
always-"running" status badge from the session header.
…eaming state

The first message in a new task blocked the composer's slide-down
animation on the full agent cold-start (process spawn + ACP handshake)
before the user turn ever entered the store. Send now materializes the
turn under a temporary draft key immediately, creates the session in
the background, and promotes the conversation onto the real id once it
resolves - marking it loaded so the workspace's reload-on-select effect
doesn't clear turns and replay the slide.

The composer's send button also now distinguishes "live turn starting
up" (spinner) from "actively streaming" (stop icon), while keeping the
click target identical in both states.
Selecting a session kicked off loadSession, which streams history into an
off-store builder that only commits once the load completes, so turns stayed
empty for the whole load. ChatView chose its layout from turns.length alone,
leaving the composer in the centred landing position until history arrived and
then sliding it down — the "stuck, then animate" delay.

Drive the landing-vs-thread decision from a new isLoading signal too: a
selected session takes the thread layout at once (composer slides on click) and
a history-loading indicator fills the thread until turns land. isEmpty now flips
true->false a single time on selection and stays false through completion, so
the FLIP slide fires exactly once and does not replay when turns arrive.
Swap the pre-first-chunk typing dots for a "still working" line that
trails the live turn through tool calls and the quiet gaps between them,
so the thread never looks frozen while the agent is busy. It leads with
the sidebar's nine-dot activity grid (now extracted to a shared
component) and a rotating, playful status word.

Hide it while the answer body itself is streaming: the growing text is
signal enough, so a second line under it just reads as noise. It returns
for thoughts, tool calls, and the waits between.
The project row primary "+" cleared selection into the empty composer;
point it at the create-worktree-task dialog instead so it creates the
task that later sessions attach to. Task rows keep the new-session action.
Creating a worktree only needs its title; status is meaningful once the
task exists, so the select now shows only when editing and new tasks
start at "todo".
The project row's task count and the worktree row's status text add
noise without navigation value; remove both meta labels.
…oolbar

Adds a split button left of the window controls that opens the current
context's directory in the file manager, a terminal, or VS Code, or copies
its path. The icon half repeats the remembered default opener (cached in
localStorage, initially Explorer); the chevron half switches it. Choosing an
opener both runs it and becomes the new default; Copy Path is menu-only and
never a default.

Backend is a desktop-only platform capability: a resolve_task_cwd command
returns a task's live git worktree directory (worktree_root/<id>) via the
same resolution the agent runtime uses, and an open_location command hands
that path to the host OS (Windows + macOS) with std::process::Command. Paths
are normalized to native separators at the source so both opening and copying
read naturally on Windows. The Web host reports the capability unsupported, so
the button stays hidden there.
  Explicitly allow text selection for editable controls, chat content,
  tool output, diffs, paths, and error messages while keeping interface
  chrome and interactive controls non-selectable.
Add POST /api/skills/import to upload a local skill folder as one
multipart request and commit it atomically across the filesystem and
the database.

Files stream into an `atoms/skills/<uuid>.tmp` staging directory, the
staged SKILL.md front matter resolves the skill name and description,
then a unit of work inserts the row, renames staging to the committed
`<name>` directory, and commits together — so a failure at any point
leaves neither a row nor a promoted directory. Startup reconciliation
discards `*.tmp` staging and any committed directory not backed by a
visible skill row, and skill deletion now removes its committed folder.

Keep the orchestration hexagonal: a SkillPackageStore port with a local
filesystem adapter, a SkillImportUnitOfWork closure port whose SQLite
adapter holds the transaction open across the directory promote, and
folder/manifest validation, all under crates/application/src/skill.
Guard uploads with a 50MB body limit, a 1000-file cap, safe-name and
safe-path checks, and empty/duplicate-path rejection, mapping failures
to 422/409/413 responses.
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.

4 participants