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
13 changes: 9 additions & 4 deletions .github/workflows/pr-gate.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,17 +6,22 @@ on:

jobs:
check-contributor:
# pull_request_target loads this workflow from the PR base ref. Skip the
# entire job on forks before actions/github-script resolves the maintainer
# token, which intentionally exists only in the canonical repository.
if: ${{ github.repository == 'ogulcancelik/herdr' }}
runs-on: ubuntu-latest
permissions:
contents: read
issues: write
pull-requests: write
steps:
# pull_request_target resolves this workflow from the repository's
# default branch. Non-canonical repository copies do not have the
# canonical maintainer token, so complete their local check without
# resolving the token-bearing action.
- name: Confirm canonical intake policy is not applicable to this repository
if: ${{ github.repository != 'ogulcancelik/herdr' }}
run: echo "PR Gate is canonical-repository-only; fork check passed."

- name: Check pull request intake policy
if: ${{ github.repository == 'ogulcancelik/herdr' }}
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9
with:
github-token: ${{ secrets.KANGAL_GITHUB_TOKEN }}
Expand Down
2 changes: 1 addition & 1 deletion docs/next/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

### Changed
- Agent status indicators now use the same static workspace marks across the sidebar, navigator, and mobile views, eliminating continuous spinner rendering while agents work.
- Desktop sessions now show a theme-derived, right-aligned native status row containing the focused folder and Git branch, device name, CPU, and memory. The left side stays blank; the row does not collect or display network details or usernames, and it does not show the Herdr version, battery, date, or time. Set `ui.status_bar.enabled = false` to hide the row and stop its background sampling.
- Desktop sessions now show a theme-derived, right-aligned native status row containing the focused folder and Git branch, device name, Herdr build version, CPU, and memory. The left side stays blank; the row does not collect or display session, workspace, tab, or pane identity, network details, usernames, battery, date, or time. Set `ui.status_bar.enabled = false` to hide the row and stop its background sampling.
- Relicensed Herdr from AGPL-3.0-or-later to Apache-2.0.

### Fixed
Expand Down
4 changes: 2 additions & 2 deletions docs/next/website/src/content/docs/configuration.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -258,9 +258,9 @@ The sidebar is the main Herdr dashboard. Search `ui.` in the [Config reference](

### Status row

Desktop layouts show a right-aligned, full-width status row above the tab bar. Its compact information order is focused folder, Git branch, device name, CPU, then memory. The left side stays blank. Literal `CPU` and `MEM` segments stay visible at desktop widths, use percent and GiB, and show `CPU --%` or `MEM --/-- GiB` when a sample is unavailable. The row does not display or sample network details or usernames, and it does not show the Herdr version, battery, date, or time. As the terminal narrows, optional segments disappear in this order: folder, branch, then device name.
Desktop layouts show a right-aligned, full-width status row above the tab bar. Its compact information order is focused folder, Git branch, device name, Herdr build version, CPU, then memory. The left side stays blank. The build version plus the literal `CPU` and `MEM` segments stay visible at desktop widths; metrics use percent and GiB and show `CPU --%` or `MEM --/-- GiB` when a sample is unavailable. The row does not display session, workspace, tab, or pane identity or sample network details or usernames, and it does not show battery, date, or time. As the terminal narrows, optional segments disappear in this order: folder, branch, then device name.

The sampler starts immediately, refreshes every two seconds with at most one sample in flight, and retries failures at the next interval. Samples that reach four seconds old are discarded so unavailable values render instead. CPU and memory metrics come from native APIs or local proc files.
The sampler starts when a renderable status row first exists, refreshes every two seconds with at most one sample in flight, and retries failures at the next interval. Samples that reach four seconds old are discarded so unavailable values render instead. CPU and memory metrics come from native APIs or local proc files.

```toml
[ui.status_bar]
Expand Down
4 changes: 2 additions & 2 deletions docs/next/website/src/content/docs/ja/configuration.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -254,9 +254,9 @@ yellow = "#f9e2af"

### ステータス行

デスクトップレイアウトでは、タブバーの上に右寄せの全幅ステータス行が表示されます。コンパクトな情報順は、フォーカス中のフォルダー、Git ブランチ、デバイス名、CPU、メモリです。左側は空白のままです。デスクトップ幅では、`CPU``MEM` のラベル付きセグメントが表示され、単位にはパーセントと GiB が使われます。サンプルを利用できない場合は `CPU --%` または `MEM --/-- GiB` と表示されます。この行はネットワーク情報やユーザー名を表示もサンプリングもせず、Herdr バージョン、バッテリー、日付、時刻も表示しません。ターミナルが狭くなると、フォルダー、Git ブランチ、デバイス名の順に省略されます。
デスクトップレイアウトでは、タブバーの上に右寄せの全幅ステータス行が表示されます。情報の順序は、フォーカス中のフォルダー、Git ブランチ、デバイス名、Herdr ビルドバージョン、CPU、メモリです。左側は空白のままです。デスクトップ幅では、ビルドバージョンと `CPU``MEM` の各セグメントが常に表示されます。メトリクスはパーセントと GiB を使い、サンプルを利用できない場合は `CPU --%` または `MEM --/-- GiB` と表示されます。セッション、ワークスペース、タブ、ペインの識別情報、ネットワーク情報、ユーザー名、バッテリー、日付、時刻は表示されません。ターミナルが狭くなると、フォルダー、ブランチ、デバイス名の順に省略されます。

サンプラーは直ちに開始し、同時に実行するサンプルを 1 つまでに制限しながら 2 秒ごとに更新します。失敗した場合は次の間隔で再試行します。4 秒に達した古いサンプルは破棄され、利用不可の値が表示されます。CPU とメモリのメトリクスには、ネイティブ API またはローカルの proc ファイルだけが使われます。
サンプラーは表示可能なステータス行が初めて存在したときに開始し、同時に実行するサンプルを 1 つまでに制限しながら 2 秒ごとに更新します。失敗した場合は次の間隔で再試行します。4 秒に達した古いサンプルは破棄され、利用不可の値が表示されます。CPU とメモリのメトリクスには、ネイティブ API またはローカルの proc ファイルだけが使われます。

```toml
[ui.status_bar]
Expand Down
4 changes: 2 additions & 2 deletions docs/next/website/src/content/docs/zh-cn/configuration.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -254,9 +254,9 @@ yellow = "#f9e2af"

### 状态行

桌面布局会在标签栏上方显示一条右对齐的全宽状态行。紧凑信息顺序为:当前聚焦文件夹、Git 分支、设备名称、CPU、内存。左侧保持空白。在桌面宽度下,带有 `CPU` 和 `MEM` 字样的片段会显示,分别使用百分比和 GiB 作为单位;样本不可用时会显示 `CPU --%` 或 `MEM --/-- GiB`。该行不会显示或采样网络信息或用户名,也不会显示 Herdr 版本、电池、日期或时间。随着终端变窄,可选片段会按以下顺序消失:文件夹、Git 分支、设备名称
桌面布局会在标签栏上方显示一条右对齐的全宽状态行。信息顺序是聚焦文件夹、Git 分支、设备名、Herdr 构建版本、CPU、内存;左侧始终留空。桌面宽度下,构建版本以及 `CPU` 和 `MEM` 片段始终可见。指标分别使用百分比和 GiB;样本不可用时显示 `CPU --%` 或 `MEM --/-- GiB`。状态行不显示会话、工作区、标签页或窗格标识、网络信息、用户名、电池、日期或时间。终端变窄时,可选片段按文件夹、分支、设备名的顺序消失

采样器会立即启动,每两秒刷新一次,同时最多只允许一个样本正在采集;失败后会在下一个间隔重试。达到四秒的旧样本会被丢弃并显示不可用值。CPU 和内存指标只来自原生 API 或本地 proc 文件。
采样器会在首次出现可渲染的状态行时启动,每两秒刷新一次,同时最多只允许一个样本正在采集;失败后会在下一个间隔重试。达到四秒的旧样本会被丢弃并显示不可用值。CPU 和内存指标只来自原生 API 或本地 proc 文件。

```toml
[ui.status_bar]
Expand Down
41 changes: 31 additions & 10 deletions scripts/test_pr_gate_workflow.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,23 +7,44 @@


class PullRequestGateWorkflowTests(unittest.TestCase):
def test_forks_skip_job_before_maintainer_token_resolution(self) -> None:
def test_forks_pass_job_without_maintainer_token_resolution(self) -> None:
workflow = WORKFLOW_PATH.read_text(encoding="utf-8")

job_start = workflow.index(" check-contributor:\n")
guard = workflow.index(
runs_on = workflow.index(" runs-on:", job_start)
self.assertNotIn(
" if: ${{ github.repository == 'ogulcancelik/herdr' }}\n",
job_start,
workflow[job_start:runs_on],
)
runs_on = workflow.index(" runs-on:", job_start)
steps = workflow.index(" steps:", job_start)

self.assertLess(job_start, guard)
self.assertLess(guard, runs_on)
self.assertLess(runs_on, steps)
self.assertIn(
no_op_step = workflow.index(
" - name: Confirm canonical intake policy is not applicable",
runs_on,
)
no_op_condition = workflow.index(
" if: ${{ github.repository != 'ogulcancelik/herdr' }}\n",
no_op_step,
)
policy_step = workflow.index(
" - name: Check pull request intake policy",
no_op_condition,
)
policy_condition = workflow.index(
" if: ${{ github.repository == 'ogulcancelik/herdr' }}\n",
policy_step,
)
token = workflow.index(
"github-token: ${{ secrets.KANGAL_GITHUB_TOKEN }}",
workflow,
policy_condition,
)

self.assertLess(no_op_step, no_op_condition)
self.assertLess(no_op_condition, policy_step)
self.assertLess(policy_step, policy_condition)
self.assertLess(policy_condition, token)
self.assertIn(
'run: echo "PR Gate is canonical-repository-only; fork check passed."',
workflow[no_op_condition:policy_step],
)


Expand Down
6 changes: 3 additions & 3 deletions src/app/api.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1459,7 +1459,7 @@ mod tests {
assert_eq!(app.state.status_git_branch, None);
assert!(app.git_identity_refresh_requested);
assert_eq!(crate::terminal::TerminalRuntime::test_cwd_query_count(), 0);
let (_, _, _, cwd, branch) = crate::ui::focused_status_identity_for_test(&app.state);
let (cwd, branch) = crate::ui::focused_status_context_for_test(&app.state);
assert_eq!(cwd, Some(std::path::PathBuf::from("/repo/nested")));
assert_eq!(branch, None);

Expand All @@ -1474,7 +1474,7 @@ mod tests {
assert_eq!(app.state.status_git_cwd, None);
assert_eq!(app.state.status_git_branch, None);
assert_eq!(crate::terminal::TerminalRuntime::test_cwd_query_count(), 0);
let (_, _, _, cwd, branch) = crate::ui::focused_status_identity_for_test(&app.state);
let (cwd, branch) = crate::ui::focused_status_context_for_test(&app.state);
assert_eq!(cwd, None);
assert_eq!(branch, None);
}
Expand Down Expand Up @@ -1523,7 +1523,7 @@ mod tests {
assert!(!app.git_identity_refresh_requested);
assert_eq!(crate::terminal::TerminalRuntime::test_cwd_query_count(), 0);
app.state.status_bar_enabled = true;
let (_, _, _, cwd, branch) = crate::ui::focused_status_identity_for_test(&app.state);
let (cwd, branch) = crate::ui::focused_status_context_for_test(&app.state);
assert_eq!(cwd, Some(std::path::PathBuf::from("/repo/nested")));
assert_eq!(branch, None);
}
Expand Down
19 changes: 11 additions & 8 deletions src/app/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,7 @@ pub struct App {
pub(crate) status_metric_sampler:
Arc<std::sync::Mutex<crate::platform::status_metrics::StatusMetricSampler>>,
pub(crate) status_metric_refresh_enabled: bool,
pub(crate) status_metrics_visible: bool,
pub(crate) terminal_runtimes: crate::terminal::TerminalRuntimeRegistry,
pub event_tx: mpsc::Sender<AppEvent>,
pub(crate) event_rx: mpsc::Receiver<AppEvent>,
Expand Down Expand Up @@ -764,6 +765,7 @@ impl App {
crate::platform::status_metrics::StatusMetricSampler::new(),
)),
status_metric_refresh_enabled: !cfg!(test),
status_metrics_visible: false,
terminal_runtimes: restored_terminal_runtimes,
event_tx,
event_rx,
Expand Down Expand Up @@ -1158,6 +1160,8 @@ impl App {
frame,
);
})?;
self.status_metrics_visible =
self.state.view.status_bar_rect != ratatui::layout::Rect::default();
if kitty_graphics_enabled {
crate::kitty_graphics::paint_local_pane_graphics(
&self.state,
Expand Down Expand Up @@ -2941,7 +2945,7 @@ mod tests {
assert_eq!(report.status, crate::config::ConfigReloadStatus::Applied);
assert!(app.state.status_bar_enabled);
assert!(app.state.status_focus_projection_initialized);
let (_, _, _, cwd, branch) = crate::ui::focused_status_identity_for_test(&app.state);
let (cwd, branch) = crate::ui::focused_status_context_for_test(&app.state);
assert_eq!(cwd, Some(std::path::PathBuf::from("/repo/nested")));
assert_eq!(branch, None);
assert_eq!(crate::terminal::TerminalRuntime::test_cwd_query_count(), 0);
Expand Down Expand Up @@ -2990,7 +2994,7 @@ mod tests {
let report = app.reload_config();

assert_eq!(report.status, crate::config::ConfigReloadStatus::Applied);
let (_, _, _, cwd, branch) = crate::ui::focused_status_identity_for_test(&app.state);
let (cwd, branch) = crate::ui::focused_status_context_for_test(&app.state);
assert_eq!(cwd, Some(std::path::PathBuf::from("/repo")));
assert_eq!(branch, None);
assert_eq!(app.state.status_git_cwd, None);
Expand Down Expand Up @@ -5246,14 +5250,13 @@ last_pane = "prefix+tab"
.terminal_id(active_pane)
.expect("active terminal");
let expected_cwd = app.state.terminals[active_terminal].cwd.clone();
let (_, _, _, stale_cwd, stale_branch) =
crate::ui::focused_status_identity_for_test(&app.state);
let (stale_cwd, stale_branch) = crate::ui::focused_status_context_for_test(&app.state);
assert_eq!(stale_cwd, Some(std::path::PathBuf::from("/old-focus")));
assert_eq!(stale_branch.as_deref(), Some("old-branch"));

crate::terminal::TerminalRuntime::test_reset_cwd_query_count();
assert!(app.sync_status_context_before_render());
let (_, _, _, cwd, branch) = crate::ui::focused_status_identity_for_test(&app.state);
let (cwd, branch) = crate::ui::focused_status_context_for_test(&app.state);
assert_eq!(cwd, Some(expected_cwd));
assert_eq!(branch, None);
assert_eq!(crate::terminal::TerminalRuntime::test_cwd_query_count(), 0);
Expand Down Expand Up @@ -5283,7 +5286,7 @@ last_pane = "prefix+tab"

crate::terminal::TerminalRuntime::test_reset_cwd_query_count();
assert!(!app.sync_status_context_before_render());
let (_, _, _, cwd, branch) = crate::ui::focused_status_identity_for_test(&app.state);
let (cwd, branch) = crate::ui::focused_status_context_for_test(&app.state);
assert_eq!(cwd, Some(std::path::PathBuf::from("/runtime")));
assert_eq!(branch.as_deref(), Some("runtime-branch"));
assert_eq!(crate::terminal::TerminalRuntime::test_cwd_query_count(), 0);
Expand Down Expand Up @@ -5329,7 +5332,7 @@ last_pane = "prefix+tab"
assert_eq!(app.status_context_focus, first_focus);
crate::terminal::TerminalRuntime::test_reset_cwd_query_count();
assert!(app.sync_status_context_before_render());
let (_, _, _, cwd, branch) = crate::ui::focused_status_identity_for_test(&app.state);
let (cwd, branch) = crate::ui::focused_status_context_for_test(&app.state);
assert_eq!(cwd, Some(std::path::PathBuf::from("/second")));
assert_eq!(branch, None);
assert_eq!(crate::terminal::TerminalRuntime::test_cwd_query_count(), 0);
Expand Down Expand Up @@ -5370,7 +5373,7 @@ last_pane = "prefix+tab"

crate::terminal::TerminalRuntime::test_reset_cwd_query_count();
assert!(!app.sync_status_context_before_render());
let (_, _, _, cwd, branch) = crate::ui::focused_status_identity_for_test(&app.state);
let (cwd, branch) = crate::ui::focused_status_context_for_test(&app.state);
assert_eq!(cwd, Some(std::path::PathBuf::from("/runtime")));
assert_eq!(branch.as_deref(), Some("runtime-branch"));
assert_eq!(crate::terminal::TerminalRuntime::test_cwd_query_count(), 0);
Expand Down
Loading
Loading