Skip to content
Open
Show file tree
Hide file tree
Changes from 11 commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
b502645
feat(tabs): select tabs by index within active space
Jun 26, 2026
af93248
Merge pull request #1 from roberto-fernandino/fix-tab-picker
roberto-fernandino Jun 26, 2026
023baa1
feat(shortcuts): add ai toggle mini shortcut
Jun 26, 2026
bda8dfb
Merge pull request #2 from roberto-fernandino/feat/toggle-ai-chat-win…
roberto-fernandino Jun 26, 2026
2b644ff
feat: terminals in command-palette
Jun 26, 2026
1a6b366
Merge pull request #3 from roberto-fernandino/feat/terminals-in-comma…
roberto-fernandino Jun 26, 2026
81e752f
feat(shortcuts): add configurable shortcut to show Files sidebar
Jun 27, 2026
df23232
Merge pull request #4 from roberto-fernandino/feat/files-sidebar-shor…
roberto-fernandino Jun 27, 2026
9bf6a2d
chore: add terax-pr-workflow project skill
Jun 27, 2026
2a5d3b2
Merge remote-tracking branch 'upstream/main'
roberto-fernandino Jun 27, 2026
b679121
feat(sidebar): add Agents tab showing all active AI agent sessions
roberto-fernandino Jun 27, 2026
a22273a
Merge pull request #6 from roberto-fernandino/feat/agents-sidebar-tab…
roberto-fernandino Jun 27, 2026
bf8faf9
fix(agents): show idle status when agent is at prompt, not working
roberto-fernandino Jun 27, 2026
fec2a7d
Merge pull request #7 from roberto-fernandino/fix/agents-idle-status
roberto-fernandino Jun 27, 2026
8a76492
feat(ai): open AI panel by default on startup
roberto-fernandino Jun 27, 2026
74b3f6c
Merge pull request #8 from roberto-fernandino/feat/ai-panel-open-by-d…
roberto-fernandino Jun 27, 2026
497e6b8
fix(agents): clear needs-input when tab focused; finished resets to idle
roberto-fernandino Jun 27, 2026
2f9e17f
Merge pull request #9 from roberto-fernandino/fix/agents-needs-input-…
roberto-fernandino Jun 27, 2026
a893d44
fix(agents): reset working status to idle when terminal tab is focused
roberto-fernandino Jun 28, 2026
1b92a83
Merge pull request #10 from roberto-fernandino/fix/agents-working-stu…
roberto-fernandino Jun 28, 2026
14c1ae1
fix(agents): use tabsRef in focus-reset effect to preserve real-time …
roberto-fernandino Jun 28, 2026
d292c64
feat(terminal): add cursor style setting (bar/block/underline)
roberto-fernandino Jun 30, 2026
f61a0c8
Merge pull request #12 from roberto-fernandino/feat/terminal-cursor-s…
roberto-fernandino Jun 30, 2026
10c67c3
chore: merge upstream/main (sidebar persist, workspace env, Linux cli…
roberto-fernandino Jun 30, 2026
e454866
feat(editor): show git blame for current line in status bar
roberto-fernandino Jun 30, 2026
5e7c13c
Merge pull request #13 from roberto-fernandino/feat/git-blame-statusbar
roberto-fernandino Jun 30, 2026
c84686d
feat(sidebar): show processes alongside agents
roberto-fernandino Jul 1, 2026
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
112 changes: 112 additions & 0 deletions .claude/skills/terax-pr-workflow/SKILL.md
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
```
Comment on lines +33 to +35

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Use pnpm instead of npx for 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) uses pnpm exec tsc --noEmit. Using npx here 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
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.claude/skills/terax-pr-workflow/SKILL.md around lines 33 - 35, The workflow
instruction currently uses npx for type-checking, which conflicts with the
repo’s pnpm-only convention. Update the type-checking command in SKILL.md to use
pnpm exec tsc --noEmit (or the existing check-types script via pnpm) and ensure
any references in this workflow doc consistently avoid npx/npm/yarn; this change
is localized to the type-checking step in the skill instructions.

Source: Coding guidelines


### 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)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

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 Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.claude/skills/terax-pr-workflow/SKILL.md at line 74, The skill document
contains an emoji in the generated-by line, which violates the no-emojis rule.
Remove the robot emoji from the affected line in SKILL.md and keep the rest of
the attribution text unchanged so the document remains compliant with the
project-wide formatting guidelines.

Source: 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`.
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
"version": "0.8.2",
"license": "Apache-2.0",
"type": "module",
"packageManager": "pnpm@11.5.0+sha512.dbfcc4f81cf48597afd4bc391ffdf12c11f1a9fb83a395bfa6b0a2d9cc2fd8ffebafdb1ccbd529632153f793904c2615b7f09fe1a345473fd1c35845172a8eb1",
"packageManager": "pnpm@11.9.0+sha512.bd682d5d03fe525ef7c9fd6780c6884d1e756ac4c9c9fe00c538782824310dcf90e3ddc4f53835f06dfaebd5085e41855e0bcbb3b60de2ac5bbab89e5036f03b",
"engines": {
"node": ">=22"
},
Expand Down
53 changes: 51 additions & 2 deletions src/app/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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,
Expand All @@ -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);
Expand All @@ -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);
Expand Down Expand Up @@ -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),
Expand All @@ -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"),

Copy link
Copy Markdown

Choose a reason for hiding this comment

The 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 cycleSidebarView("explorer"), which collapses the sidebar when Files is already open. That makes the new shortcut and palette command non idempotent and can hide the explorer on repeated use.

Also applies to: 1014-1014

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/app/App.tsx` at line 687, The “Show Files” command is currently routed
through the toggle-style sidebar helper, so repeated invocations can collapse
the explorer instead of keeping Files open. Update the `sidebar.files` command
in `App.tsx` and the matching palette shortcut path to call the sidebar state
setter directly (or otherwise force the explorer view) instead of
`cycleSidebarView("explorer")`, using the existing sidebar/view logic in `App`
so the action is idempotent.

"terminal.clear": () => {
clearFocusedTerminal();
},
Expand All @@ -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();
Expand Down Expand Up @@ -700,7 +730,9 @@ export default function App() {
splitActivePaneInActiveTab,
focusNextPaneInTab,
toggleSourceControl,
hasComposer,
togglePanelAndFocus,
toggleMini,
askFromSelection,
toggleSidebar,
toggleExplorerFocus,
Expand Down Expand Up @@ -979,6 +1011,7 @@ export default function App() {
openNewPreview: () => openPreviewTab(""),
openGitGraph: openGitGraphFromContext,
toggleSourceControl,
toggleFilesExplorer: () => cycleSidebarView("explorer"),
closeActiveTabOrPane: handleCloseTabOrPane,
splitPaneRight: () => splitActivePaneInActiveTab("row"),
splitPaneDown: () => splitActivePaneInActiveTab("col"),
Expand All @@ -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),
})
: [],
[
Expand All @@ -1009,6 +1048,7 @@ export default function App() {
openPreviewTab,
openGitGraphFromContext,
toggleSourceControl,
cycleSidebarView,
handleCloseTabOrPane,
splitActivePaneInActiveTab,
toggleSidebar,
Expand Down Expand Up @@ -1082,6 +1122,7 @@ export default function App() {
searchTarget={searchTarget}
searchRef={searchInlineRef}
onOverrideLanguage={setOverrideLanguage}
agentsByTabId={agentsByTabId}
/>
)}

Expand Down Expand Up @@ -1118,6 +1159,12 @@ export default function App() {
onRevealInTerminal={cdInNewTab}
onAttachToAgent={handleAttachFileToAgent}
/>
) : sidebarView === "agents" ? (
<AgentsPanel
sessions={agentSessions}
tabs={tabs}
onSelectTab={setActiveId}
/>
Comment thread
coderabbitai[bot] marked this conversation as resolved.
) : (
<SourceControlPanel
open
Expand All @@ -1133,6 +1180,8 @@ export default function App() {
activeView={sidebarView}
onSelectView={persistSidebarView}
changedCount={sourceControl.changedCount}
showAgentsTab={showAgentsTab}
agentCount={agentSessions.length}
/>
</div>
</ResizablePanel>
Expand Down
111 changes: 111 additions & 0 deletions src/modules/agents-panel/AgentsPanel.tsx
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;
Comment thread
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>
);
}
1 change: 1 addition & 0 deletions src/modules/agents-panel/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { AgentsPanel } from "./AgentsPanel";
Loading