feat(skill): add atomic skill folder import#68
Open
ObsisMc wants to merge 28 commits into
Open
Conversation
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.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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>.tmpstaging 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*.tmpstaging 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.