-
Notifications
You must be signed in to change notification settings - Fork 884
feat(sidebar): Agents tab + agent logos in terminal tabs #891
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from 11 commits
b502645
af93248
023baa1
bda8dfb
2b644ff
1a6b366
81e752f
df23232
9bf6a2d
2a5d3b2
b679121
a22273a
bf8faf9
fec2a7d
8a76492
74b3f6c
497e6b8
2f9e17f
a893d44
1b92a83
14c1ae1
d292c64
f61a0c8
10c67c3
e454866
5e7c13c
c84686d
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,112 @@ | ||
| --- | ||
| name: terax-pr-workflow | ||
| description: Use this skill whenever making ANY change to this repository — features, fixes, shortcuts, refactors, anything. It enforces the mandatory branch → commit → dual-PR (fork + upstream) → merge-fork → back-to-main workflow. Trigger on phrases like "create a PR", "add a feature", "fix this", "push this", "open a pull request", or any task that involves committing and shipping code changes. | ||
| user-invocable: true | ||
| --- | ||
|
|
||
| # Terax PR Workflow | ||
|
|
||
| Every change to this repo MUST follow this exact workflow. No exceptions. | ||
|
|
||
| ## Remotes | ||
|
|
||
| - `origin` → `https://github.com/roberto-fernandino/terax-ai-fork.git` (the fork — you have write access) | ||
| - `upstream` → `https://github.com/crynta/terax-ai.git` (the original — NO push access, use cross-fork PRs) | ||
|
|
||
| ## Step-by-step | ||
|
|
||
| ### 1. Create a branch | ||
|
|
||
| ```bash | ||
| git checkout main | ||
| git pull origin main | ||
| git checkout -b feat/<descriptive-name> | ||
| # or fix/<name>, chore/<name>, etc. | ||
| ``` | ||
|
|
||
| Branch name must be kebab-case and describe the change. | ||
|
|
||
| ### 2. Make the changes | ||
|
|
||
| Implement the feature/fix. Type-check before committing: | ||
|
|
||
| ```bash | ||
| npx tsc --noEmit | ||
| ``` | ||
|
|
||
| ### 3. Commit | ||
|
|
||
| Stage only the relevant files (never `git add -A` blindly): | ||
|
|
||
| ```bash | ||
| git add <specific files> | ||
| git commit -m "$(cat <<'EOF' | ||
| feat/fix/chore(scope): short imperative description | ||
|
|
||
| Longer explanation if needed. | ||
|
|
||
| Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> | ||
| EOF | ||
| )" | ||
| ``` | ||
|
|
||
| ### 4. Push to fork | ||
|
|
||
| ```bash | ||
| git push origin <branch-name> | ||
| ``` | ||
|
|
||
| ### 5. Create PR on the fork (roberto-fernandino/terax-ai-fork) | ||
|
|
||
| ```bash | ||
| gh pr create \ | ||
| --repo roberto-fernandino/terax-ai-fork \ | ||
| --base main \ | ||
| --head <branch-name> \ | ||
| --title "<same as commit title>" \ | ||
| --body "$(cat <<'EOF' | ||
| ## Summary | ||
| - bullet points | ||
|
|
||
| ## Test plan | ||
| - [ ] item | ||
|
|
||
| 🤖 Generated with [Claude Code](https://claude.com/claude-code) | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Remove emoji from skill document. The robot emoji violates the project-wide "No emojis anywhere" rule that applies to code, comments, commits, and docs. As per coding guidelines, no emojis anywhere. 🤖 Prompt for AI AgentsSource: Coding guidelines |
||
| EOF | ||
| )" | ||
| ``` | ||
|
|
||
| ### 6. Create cross-fork PR on the upstream (crynta/terax-ai) | ||
|
|
||
| Push access is denied to upstream, so use the fork branch as head: | ||
|
|
||
| ```bash | ||
| gh pr create \ | ||
| --repo crynta/terax-ai \ | ||
| --base main \ | ||
| --head roberto-fernandino:<branch-name> \ | ||
| --title "<same as commit title>" \ | ||
| --body "..." | ||
| ``` | ||
|
|
||
| ### 7. Merge the fork PR | ||
|
|
||
| ```bash | ||
| gh pr merge <pr-number> --repo roberto-fernandino/terax-ai-fork --merge --auto | ||
| ``` | ||
|
|
||
| ### 8. Return to main and pull | ||
|
|
||
| ```bash | ||
| git checkout main | ||
| git pull origin main | ||
| ``` | ||
|
|
||
| ## Rules | ||
|
|
||
| - ALWAYS create a branch — never commit directly to main. | ||
| - ALWAYS open TWO PRs: one on the fork, one on the upstream (cross-fork). | ||
| - ALWAYS merge the fork PR and return to main at the end. | ||
| - NEVER push directly to upstream (no access); always use `--head roberto-fernandino:<branch>` for the upstream PR. | ||
| - Type-check (`npx tsc --noEmit`) before committing. | ||
| - Use conventional commit prefixes: `feat`, `fix`, `chore`, `refactor`, `docs`. | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -11,6 +11,8 @@ import { quoteShellArg } from "@/lib/shellQuote"; | |
| import { useZoom } from "@/lib/useZoom"; | ||
| import { getCurrentWindow } from "@tauri-apps/api/window"; | ||
| import { AgentNotificationsBridge, nextAttentionTarget } from "@/modules/agents"; | ||
| import { useAgentStore } from "@/modules/agents/store/agentStore"; | ||
| import { AgentsPanel } from "@/modules/agents-panel"; | ||
| import { | ||
| AgentRunBridge, | ||
| AiMiniWindow, | ||
|
|
@@ -258,6 +260,21 @@ export default function App() { | |
| [tabs, activeSpaceId], | ||
| ); | ||
|
|
||
| const agentStoreSessions = useAgentStore((s) => s.sessions); | ||
| const agentSessions = useMemo( | ||
| () => Object.values(agentStoreSessions), | ||
| [agentStoreSessions], | ||
| ); | ||
| const agentsByTabId = useMemo(() => { | ||
| const map = new Map<number, string>(); | ||
| for (const s of agentSessions) { | ||
| map.set(s.tabId, s.agent); | ||
| } | ||
| return map; | ||
| }, [agentSessions]); | ||
|
|
||
| const showAgentsTab = usePreferencesStore((s) => s.showAgentsTab); | ||
|
|
||
| const { | ||
| sidebarRef, | ||
| sidebarWidthRef, | ||
|
|
@@ -267,7 +284,7 @@ export default function App() { | |
| cycleSidebarView, | ||
| persistSidebarWidth, | ||
| toggleExplorerFocus, | ||
| } = useSidebarPanel(explorerRef); | ||
| } = useSidebarPanel(explorerRef, showAgentsTab); | ||
|
|
||
| const [newEditorOpen, setNewEditorOpen] = useState(false); | ||
| const [commandPaletteOpen, setCommandPaletteOpen] = useState(false); | ||
|
|
@@ -284,6 +301,7 @@ export default function App() { | |
| const miniOpen = useChatStore((s) => s.mini.open); | ||
| const miniPresence = usePresence(miniOpen, 200); | ||
| const openMini = useChatStore((s) => s.openMini); | ||
| const toggleMini = useChatStore((s) => s.toggleMini); | ||
| const focusInput = useChatStore((s) => s.focusInput); | ||
| const openPanel = useChatStore((s) => s.openPanel); | ||
| const panelOpen = useChatStore((s) => s.panelOpen); | ||
|
|
@@ -653,7 +671,11 @@ export default function App() { | |
| "tab.close": handleCloseTabOrPane, | ||
| "tab.next": () => stepSwitcher(1), | ||
| "tab.prev": () => stepSwitcher(-1), | ||
| "tab.selectByIndex": (e) => selectByIndex(parseInt(e.key, 10) - 1), | ||
| "tab.selectByIndex": (e) => | ||
| selectByIndex( | ||
| parseInt(e.key, 10) - 1, | ||
| activeSpaceId ?? DEFAULT_SPACE_ID, | ||
| ), | ||
| "space.next": () => cycleSpace(1), | ||
| "space.prev": () => cycleSpace(-1), | ||
| "space.overview": () => setSwitcherOpen(true), | ||
|
|
@@ -662,6 +684,7 @@ export default function App() { | |
| "pane.focusNext": () => focusNextPaneInTab(activeId, 1), | ||
| "pane.focusPrev": () => focusNextPaneInTab(activeId, -1), | ||
| "pane.source": toggleSourceControl, | ||
| "sidebar.files": () => cycleSidebarView("explorer"), | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win "Show Files" is wired through a toggle path. Both call sites use Also applies to: 1014-1014 🤖 Prompt for AI Agents |
||
| "terminal.clear": () => { | ||
| clearFocusedTerminal(); | ||
| }, | ||
|
|
@@ -671,6 +694,13 @@ export default function App() { | |
| "blocks.next": () => navigateFocusedBlocks(1), | ||
| "search.focus": () => searchInlineRef.current?.focus(), | ||
| "ai.toggle": togglePanelAndFocus, | ||
| "ai.toggleMini": () => { | ||
| if (!hasComposer) { | ||
| void openSettingsWindow("models"); | ||
| return; | ||
| } | ||
| toggleMini(); | ||
| }, | ||
| "ai.askSelection": askFromSelection, | ||
| "agent.focusAttention": () => { | ||
| const t = nextAttentionTarget(); | ||
|
|
@@ -700,7 +730,9 @@ export default function App() { | |
| splitActivePaneInActiveTab, | ||
| focusNextPaneInTab, | ||
| toggleSourceControl, | ||
| hasComposer, | ||
| togglePanelAndFocus, | ||
| toggleMini, | ||
| askFromSelection, | ||
| toggleSidebar, | ||
| toggleExplorerFocus, | ||
|
|
@@ -979,6 +1011,7 @@ export default function App() { | |
| openNewPreview: () => openPreviewTab(""), | ||
| openGitGraph: openGitGraphFromContext, | ||
| toggleSourceControl, | ||
| toggleFilesExplorer: () => cycleSidebarView("explorer"), | ||
| closeActiveTabOrPane: handleCloseTabOrPane, | ||
| splitPaneRight: () => splitActivePaneInActiveTab("row"), | ||
| splitPaneDown: () => splitActivePaneInActiveTab("col"), | ||
|
|
@@ -994,6 +1027,12 @@ export default function App() { | |
| openSpacesOverview: () => setSwitcherOpen(true), | ||
| newSpace: () => void handleNewSpace(), | ||
| switchSpace: (id) => useSpaces.getState().setActive(id), | ||
| terminalTabs: tabs.filter( | ||
| (t) => | ||
| t.kind === "terminal" && | ||
| t.spaceId === (activeSpaceId ?? DEFAULT_SPACE_ID), | ||
| ), | ||
| switchTab: (id) => setActiveId(id), | ||
| }) | ||
| : [], | ||
| [ | ||
|
|
@@ -1009,6 +1048,7 @@ export default function App() { | |
| openPreviewTab, | ||
| openGitGraphFromContext, | ||
| toggleSourceControl, | ||
| cycleSidebarView, | ||
| handleCloseTabOrPane, | ||
| splitActivePaneInActiveTab, | ||
| toggleSidebar, | ||
|
|
@@ -1082,6 +1122,7 @@ export default function App() { | |
| searchTarget={searchTarget} | ||
| searchRef={searchInlineRef} | ||
| onOverrideLanguage={setOverrideLanguage} | ||
| agentsByTabId={agentsByTabId} | ||
| /> | ||
| )} | ||
|
|
||
|
|
@@ -1118,6 +1159,12 @@ export default function App() { | |
| onRevealInTerminal={cdInNewTab} | ||
| onAttachToAgent={handleAttachFileToAgent} | ||
| /> | ||
| ) : sidebarView === "agents" ? ( | ||
| <AgentsPanel | ||
| sessions={agentSessions} | ||
| tabs={tabs} | ||
| onSelectTab={setActiveId} | ||
| /> | ||
|
coderabbitai[bot] marked this conversation as resolved.
|
||
| ) : ( | ||
| <SourceControlPanel | ||
| open | ||
|
|
@@ -1133,6 +1180,8 @@ export default function App() { | |
| activeView={sidebarView} | ||
| onSelectView={persistSidebarView} | ||
| changedCount={sourceControl.changedCount} | ||
| showAgentsTab={showAgentsTab} | ||
| agentCount={agentSessions.length} | ||
| /> | ||
| </div> | ||
| </ResizablePanel> | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,111 @@ | ||
| import { AgentIcon } from "@/modules/agents/lib/agentIcon"; | ||
| import { displayAgent } from "@/modules/agents/lib/format"; | ||
| import type { AgentSession, AgentStatus } from "@/modules/agents/lib/types"; | ||
| import type { Tab } from "@/modules/tabs/lib/useTabs"; | ||
| import { cn } from "@/lib/utils"; | ||
|
|
||
| type Props = { | ||
| sessions: AgentSession[]; | ||
| tabs: Tab[]; | ||
| onSelectTab: (tabId: number) => void; | ||
|
coderabbitai[bot] marked this conversation as resolved.
Outdated
|
||
| }; | ||
|
|
||
| function StatusBadge({ status }: { status: AgentStatus }) { | ||
| return ( | ||
| <span | ||
| className={cn( | ||
| "inline-flex items-center rounded-full px-1.5 py-0.5 text-[9px] font-medium leading-none", | ||
| status === "waiting" | ||
| ? "bg-amber-500/15 text-amber-600 dark:text-amber-400" | ||
| : "bg-emerald-500/15 text-emerald-600 dark:text-emerald-400", | ||
| )} | ||
| > | ||
| {status === "waiting" ? "needs input" : "working"} | ||
| </span> | ||
| ); | ||
| } | ||
|
|
||
| function tabTitleFor(tabs: Tab[], tabId: number): string | null { | ||
| const t = tabs.find((x) => x.id === tabId); | ||
| if (!t) return null; | ||
| if (t.kind === "terminal") return t.customTitle ?? t.title ?? null; | ||
| return null; | ||
| } | ||
|
|
||
| export function AgentsPanel({ sessions, tabs, onSelectTab }: Props) { | ||
| if (sessions.length === 0) { | ||
| return ( | ||
| <div className="flex h-full flex-col"> | ||
| <PanelHeader /> | ||
| <div className="flex flex-1 flex-col items-center justify-center gap-2 px-4 text-center"> | ||
| <span className="text-[11.5px] text-muted-foreground"> | ||
| No active agents. | ||
| </span> | ||
| <span className="text-[10.5px] leading-relaxed text-muted-foreground/70"> | ||
| Start{" "} | ||
| <code className="rounded bg-muted/50 px-1 font-mono">claude</code>,{" "} | ||
| <code className="rounded bg-muted/50 px-1 font-mono">codex</code>,{" "} | ||
| <code className="rounded bg-muted/50 px-1 font-mono">gemini</code>, | ||
| or{" "} | ||
| <code className="rounded bg-muted/50 px-1 font-mono">opencode</code>{" "} | ||
| in a terminal, or use{" "} | ||
| <code className="rounded bg-muted/50 px-1 font-mono"> | ||
| /claude-code | ||
| </code>{" "} | ||
| in the AI chat. | ||
| </span> | ||
| </div> | ||
| </div> | ||
| ); | ||
| } | ||
|
|
||
| return ( | ||
| <div className="flex h-full min-h-0 flex-col"> | ||
| <PanelHeader count={sessions.length} /> | ||
| <ul className="min-h-0 flex-1 overflow-y-auto p-1.5"> | ||
| {sessions.map((s) => { | ||
| const tabTitle = tabTitleFor(tabs, s.tabId); | ||
| return ( | ||
| <li key={s.leafId}> | ||
| <button | ||
| type="button" | ||
| onClick={() => onSelectTab(s.tabId)} | ||
| className="flex w-full items-center gap-2.5 rounded-md px-2.5 py-2 text-left transition-colors hover:bg-foreground/[0.05] active:bg-foreground/[0.08]" | ||
| > | ||
| <AgentIcon agent={s.agent} size={15} className="shrink-0" /> | ||
| <div className="min-w-0 flex-1"> | ||
| <div className="flex items-center gap-1.5"> | ||
| <span className="truncate text-[12px] font-medium"> | ||
| {displayAgent(s.agent)} | ||
| </span> | ||
| <StatusBadge status={s.status} /> | ||
| </div> | ||
| {tabTitle ? ( | ||
| <span className="block truncate text-[10.5px] text-muted-foreground"> | ||
| {tabTitle} | ||
| </span> | ||
| ) : null} | ||
| </div> | ||
| </button> | ||
| </li> | ||
| ); | ||
| })} | ||
| </ul> | ||
| </div> | ||
| ); | ||
| } | ||
|
|
||
| function PanelHeader({ count }: { count?: number }) { | ||
| return ( | ||
| <div className="flex items-center justify-between border-b border-border/60 px-3 py-2"> | ||
| <span className="text-[11px] font-semibold uppercase tracking-wider text-muted-foreground"> | ||
| Agents | ||
| </span> | ||
| {count !== undefined && count > 0 ? ( | ||
| <span className="inline-flex h-4 min-w-4 items-center justify-center rounded-full bg-primary/15 px-1 text-[9px] font-semibold tabular-nums text-primary"> | ||
| {count} | ||
| </span> | ||
| ) : null} | ||
| </div> | ||
| ); | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1 @@ | ||
| export { AgentsPanel } from "./AgentsPanel"; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Use
pnpminstead ofnpxfor type-checking.The project conventions mandate pnpm exclusively, never npm/npx. The repo already defines
"check-types": "tsc --noEmit"in package.json and all other documentation (PR template, CONTRIBUTING.md) usespnpm exec tsc --noEmit. Usingnpxhere risks version skew from a globally-installed tsc and breaks workflow consistency. As per coding guidelines, pnpm only, never npm/npx/yarn.🤖 Prompt for AI Agents
Source: Coding guidelines