Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
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
15 changes: 5 additions & 10 deletions .claude/skills/finish-worktree/SKILL.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
---
name: finish-worktree
description: Commit all changes in the current worktree and open a PR targeting the release branch for the current Cargo.toml version. Usage: /finish-worktree [release-branch] — defaults to release/v<CARGO_VERSION>.
description: Commit all changes in the current worktree and open a PR targeting the shared release branch. Usage: /finish-worktree [release-branch] — defaults to "release/next".
argument-hint: "[release-branch]"
---

Expand All @@ -20,13 +20,8 @@ Run these in parallel:

### 2. Determine the release branch

If the user provided an argument (e.g. `/finish-worktree release/v1.2.0`), use that as the release branch name.

Otherwise read the version from `Cargo.toml`:
```
grep '^version' Cargo.toml | head -1 | sed 's/.*= *"\(.*\)"/\1/'
```
Use `release/v<version>` (e.g. `release/v0.1.0`) as the release branch name.
If the user provided an argument (e.g. `/finish-worktree release/v2`), use that as the release branch name.
Otherwise use `release/next` as the default.

### 3. Ensure the release branch exists on the remote

Expand Down Expand Up @@ -68,7 +63,7 @@ EOF
)"
```

If there is nothing to commit (working tree clean), skip to step 6.
If there is nothing to commit (working tree clean), skip to step 5.

### 6. Push the current branch to remote

Expand Down Expand Up @@ -105,4 +100,4 @@ If a PR already exists for this branch (`gh pr list --head <branch>`), skip crea
Print a short summary:
- Commit hash (or "nothing to commit")
- PR URL
- Release branch targeted
- Release branch targeted
82 changes: 0 additions & 82 deletions .claude/skills/ship-release/SKILL.md

This file was deleted.

31 changes: 3 additions & 28 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
@@ -1,10 +1,6 @@
name: Release AppImage

on:
pull_request:
types: [closed]
branches:
- main
push:
tags:
- "v*"
Expand All @@ -14,32 +10,11 @@ permissions:

jobs:
build-appimage:
if: >
(github.event_name == 'push') ||
(github.event_name == 'pull_request' &&
github.event.pull_request.merged == true &&
startsWith(github.event.pull_request.head.ref, 'release/'))
runs-on: ubuntu-22.04

steps:
- uses: actions/checkout@v4

- name: Resolve version tag
id: version
run: |
if [[ "${{ github.event_name }}" == "push" ]]; then
echo "tag=${{ github.ref_name }}" >> "$GITHUB_OUTPUT"
else
VERSION=$(grep '^version' Cargo.toml | head -1 | sed 's/.*= *"\(.*\)"/\1/')
echo "tag=v${VERSION}" >> "$GITHUB_OUTPUT"
fi

- name: Create and push tag
if: github.event_name == 'pull_request'
run: |
git tag "${{ steps.version.outputs.tag }}"
git push origin "${{ steps.version.outputs.tag }}"

- name: Install system dependencies
run: |
sudo apt-get update
Expand Down Expand Up @@ -104,11 +79,11 @@ jobs:
env:
ARCH: x86_64
run: |
appimagetool --appimage-extract-and-run AppDir sheesh-rs-${{ steps.version.outputs.tag }}-x86_64.AppImage
# appimagetool needs FUSE; use extract-and-run workaround in CI
appimagetool --appimage-extract-and-run AppDir sheesh-rs-${{ github.ref_name }}-x86_64.AppImage

- name: Create GitHub Release
uses: softprops/action-gh-release@v2
with:
tag_name: ${{ steps.version.outputs.tag }}
files: sheesh-rs-${{ steps.version.outputs.tag }}-x86_64.AppImage
files: sheesh-rs-${{ github.ref_name }}-x86_64.AppImage
generate_release_notes: true
2 changes: 1 addition & 1 deletion Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "sheesh-rs"
version = "1.0.5"
version = "0.1.0"
edition = "2024"

[dependencies]
Expand Down
7 changes: 3 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ A terminal UI for managing SSH connections with an embedded LLM assistant.

- **Connection manager** — CRUD SSH connections backed by `~/.ssh/config`; comments above a `Host` block become its description
- **Embedded terminal** — connects over a PTY so the full SSH session runs inside the TUI; resizes with the window
- **LLM sidebar** — chat with an AI assistant while connected; Claude automatically reads terminal output via the `read_terminal` tool when you ask about what's on screen
- **LLM sidebar** — chat with an AI assistant while connected; press `F3` to send the last 50 terminal lines as context
- **Tool use** — Claude can propose shell commands to run on your remote session; you approve each one before it executes
- **Multi-provider LLM** — Anthropic (default), OpenAI, or a local Ollama instance
- **System prompt** — a built-in prompt configures Claude as an SSH/Linux assistant; override it in config
Expand All @@ -37,9 +37,7 @@ cargo build --release

### Release a new version (maintainers)

Merge a `release/v<version>` branch into `main` — the CI workflow tags the commit and publishes the AppImage automatically.

Alternatively, push a tag directly:
Tag a commit — the CI workflow builds and publishes the AppImage automatically:

```bash
git tag v1.0.0
Expand Down Expand Up @@ -78,6 +76,7 @@ ollama_model = "llama3"
| `a / e / d` | Listing | Add / Edit / Delete |
| `/` | Listing | Filter |
| `F2` | Connected | Switch panel (terminal ↔ LLM) |
| `F3` | Connected | Send last 50 terminal lines to LLM |
| `ctrl+d` | Terminal | Disconnect |
| `ctrl+up / down` | Terminal or LLM | Scroll history |
| `enter` | LLM | Send message |
Expand Down
2 changes: 0 additions & 2 deletions src/event.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,6 @@ pub enum Action {
Disconnect,
/// Send a command string to the terminal PTY (no trailing newline).
SendToTerminal(String),
/// Cancel an in-progress tool call and return to the user prompt.
CancelToolCall,
/// No-op
None,
}
85 changes: 22 additions & 63 deletions src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -30,14 +30,12 @@ use tabs::{Tab, listing::ListingTab, llm::LLMTab, terminal::TerminalTab};
use ui::{keybindings::render_keybindings, theme::Theme};

/// Captures terminal output produced by a tool-call command and forwards it
/// to the LLM once the output has been stable (no new lines) for a short period.
/// to the LLM once the deadline elapses.
struct PendingCapture {
/// Number of terminal lines present *before* the command was sent.
snapshot: usize,
/// Line count at the last tick where output was still growing.
last_line_count: usize,
/// When the line count last changed (used to detect output stability).
last_change: std::time::Instant,
/// When to stop waiting and send whatever has been captured so far.
deadline: std::time::Instant,
}

struct Sheesh {
Expand Down Expand Up @@ -94,11 +92,7 @@ impl Sheesh {
let provider = build_provider(&self.llm_config);
let output_log = terminal.output_log_arc();
self.terminal = Some(terminal);
let mut llm = LLMTab::new(
provider,
self.llm_config.system_prompt.clone(),
conn.clone(),
);
let mut llm = LLMTab::new(provider, self.llm_config.system_prompt.clone(), conn.clone());
llm.set_terminal_output(output_log);
self.llm = Some(llm);
self.state = AppState::Connected {
Expand Down Expand Up @@ -199,28 +193,17 @@ impl Sheesh {
match action {
Action::Quit => return false,
Action::Disconnect => self.disconnect(),
Action::CancelToolCall => {
self.pending_capture = None;
if let Some(llm) = &mut self.llm {
llm.cancel_tool_call();
}
if let Some(terminal) = &mut self.terminal {
terminal.set_tool_locked(false);
}
}
Action::SendToTerminal(cmd) => {
if let Some(t) = &mut self.terminal {
let snapshot = t.line_count();
t.send_string(&cmd);
t.send_string("\r");
t.set_tool_locked(true);
// Wait for output to stabilise (300 ms of silence) then
// forward it to Claude. The user can press ctrl+c to cancel.
let now = std::time::Instant::now();
// Capture output for 1.5 s then forward it to Claude.
self.pending_capture = Some(PendingCapture {
snapshot,
last_line_count: snapshot,
last_change: now,
deadline: std::time::Instant::now()
+ std::time::Duration::from_millis(1500),
});
}
if let AppState::Connected { ref mut focus, .. } = self.state {
Expand Down Expand Up @@ -306,9 +289,11 @@ impl Sheesh {
.as_ref()
.map(|t| t.key_hints())
.unwrap_or_default(),
ConnectedFocus::LLM => {
self.llm.as_ref().map(|l| l.key_hints()).unwrap_or_default()
}
ConnectedFocus::LLM => self
.llm
.as_ref()
.map(|l| l.key_hints())
.unwrap_or_default(),
};
hints.extend(panel_hints);
hints.push(("ctrl+q", "quit"));
Expand Down Expand Up @@ -381,26 +366,12 @@ fn main() -> anyhow::Result<()> {
loop {
terminal.draw(|f| app.draw(f))?;

// Forward captured terminal output to Claude once output has been
// stable (no new PTY lines) for 300 ms.
let should_fire = if let Some(ref mut cap) = app.pending_capture {
let now = std::time::Instant::now();
let current = app.terminal.as_ref().map_or(0, |t| t.line_count());
if current > cap.last_line_count {
cap.last_line_count = current;
cap.last_change = now;
}
let silence = now.duration_since(cap.last_change);
let has_output = cap.last_line_count > cap.snapshot;
// Wait for output to appear, then stabilise for 300 ms.
// If the command produces no output at all, fire after 5 s.
(has_output && silence >= Duration::from_millis(300))
|| (!has_output && silence >= Duration::from_secs(5))
} else {
false
};
if should_fire {
let snapshot = app.pending_capture.take().unwrap().snapshot;
// Forward captured terminal output to Claude once the deadline elapses.
if let Some(ref cap) = app.pending_capture
&& std::time::Instant::now() >= cap.deadline
{
let snapshot = cap.snapshot;
app.pending_capture = None;
if let (Some(terminal), Some(llm)) = (&app.terminal, &mut app.llm)
&& llm.awaiting_output_id.is_some()
{
Expand All @@ -411,9 +382,7 @@ fn main() -> anyhow::Result<()> {

// Release the tool lock once the LLM finishes the tool-execution cycle.
if let (Some(terminal), Some(llm)) = (&mut app.terminal, &app.llm)
&& terminal.tool_locked
&& !llm.is_executing_tool()
&& !llm.waiting
&& terminal.tool_locked && !llm.is_executing_tool() && !llm.waiting
{
terminal.set_tool_locked(false);
}
Expand Down Expand Up @@ -444,10 +413,7 @@ fn load_llm_config() -> LLMConfig {

match std::fs::read_to_string(&path) {
Err(e) => {
log::warn!(
"[config] could not read config file: {} — using defaults",
e
);
log::warn!("[config] could not read config file: {} — using defaults", e);
}
Ok(content) => {
#[derive(serde::Deserialize, Default)]
Expand All @@ -457,17 +423,10 @@ fn load_llm_config() -> LLMConfig {
}
match toml::from_str::<ConfigFile>(&content) {
Err(e) => {
log::error!(
"[config] failed to parse config.toml: {} — using defaults",
e
);
log::error!("[config] failed to parse config.toml: {} — using defaults", e);
}
Ok(cfg) => {
log::info!(
"[config] loaded: provider={} model={}",
cfg.llm.provider,
cfg.llm.model
);
log::info!("[config] loaded: provider={} model={}", cfg.llm.provider, cfg.llm.model);
return cfg.llm;
}
}
Expand Down
Loading