generated from nhcarrigan/template
feat: stuffy feature bundle (#159)
## Summary This PR bundles a collection of new features and quality-of-life improvements identified during a Claude CLI 2.1.50 audit. - **Tab status indicator** — Tab stays yellow until the greeting is responded to, then turns green. Fixed disconnect not resetting to grey. Closes #157 - **Auth status display** — New "Account" section in settings sidebar showing login status, email, org, API key source, and Hikari override indicator. Includes login/logout buttons. Closes #153 - **CLI version badge** — New "Supported" badge showing the highest audited CLI version, colour-coded green/amber/red based on installed vs supported version. Closes #154 (bump to 2.1.50) - **Rate limit events** — `rate_limit_event` messages from the stream are now parsed and shown as amber `[rate-limit]` lines in the terminal instead of being silently dropped. Closes #155 - **"Prompt is too long" handling** — Detects this error in assistant messages and shows a ⚡ Compact Conversation button to send `/compact` directly. Closes #158 - **`last_assistant_message` in Agent Monitor** — Extracts the agent's final output from the `ToolResult` content block in the JSON stream and displays it as a snippet on completed agent cards. Closes #156 - **`--worktree` flag** — New "Worktree isolation" toggle in session settings passes `--worktree` to Claude Code. Hook events (`WorktreeCreate`/`WorktreeRemove`) are displayed as green `[worktree]` lines. Closes #152, Closes #150 - **ConfigChange hook events** — `[ConfigChange Hook]` stderr events are now displayed as cyan `[config]` lines instead of errors. Closes #151 - **`CLAUDE_CODE_DISABLE_1M_CONTEXT` toggle** — New "Disable 1M context" setting in session configuration injects this env var into the Claude process. Closes #154 ## Test plan - [ ] Tab status indicator: start a new session and verify the tab stays yellow until Claude responds to the greeting, then turns green - [ ] Auth status: open settings and verify the Account section shows correct login info - [ ] CLI version badge: verify the "Supported 2.1.50" badge shows green when CLI matches - [ ] Rate limit events: unit tests cover parsing; amber `[rate-limit]` lines display correctly - [ ] Compact button: unit tests cover detection; button renders correctly in terminal - [ ] Agent Monitor: use the Task tool and verify completed agent cards show a message snippet - [ ] Worktree: enable toggle, start session, verify `--worktree` flag appears in process args - [ ] ConfigChange: hook events display as `[config]` lines rather than errors - [ ] Disable 1M context: enable toggle, start session, verify `CLAUDE_CODE_DISABLE_1M_CONTEXT=1` in `/proc/<pid>/environ` ✨ This PR was created with help from Hikari~ 🌸 Reviewed-on: #159 Co-authored-by: Hikari <hikari@nhcarrigan.com> Co-committed-by: Hikari <hikari@nhcarrigan.com>
This commit was merged in pull request #159.
This commit is contained in:
@@ -63,6 +63,26 @@ pub struct PermissionDenial {
|
||||
pub tool_input: serde_json::Value,
|
||||
}
|
||||
|
||||
/// Rate limit information from a `rate_limit_event` message.
|
||||
/// All fields are optional to ensure forward-compatibility as the Claude CLI evolves.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
|
||||
pub struct RateLimitInfo {
|
||||
#[serde(default)]
|
||||
pub requests_limit: Option<u64>,
|
||||
#[serde(default)]
|
||||
pub requests_remaining: Option<u64>,
|
||||
#[serde(default)]
|
||||
pub requests_reset: Option<String>,
|
||||
#[serde(default)]
|
||||
pub tokens_limit: Option<u64>,
|
||||
#[serde(default)]
|
||||
pub tokens_remaining: Option<u64>,
|
||||
#[serde(default)]
|
||||
pub tokens_reset: Option<String>,
|
||||
#[serde(default)]
|
||||
pub retry_after_ms: Option<u64>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[serde(tag = "type")]
|
||||
pub enum ClaudeMessage {
|
||||
@@ -100,6 +120,11 @@ pub enum ClaudeMessage {
|
||||
#[serde(default)]
|
||||
usage: Option<UsageInfo>,
|
||||
},
|
||||
#[serde(rename = "rate_limit_event")]
|
||||
RateLimitEvent {
|
||||
#[serde(default)]
|
||||
rate_limit_info: RateLimitInfo,
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
@@ -280,6 +305,8 @@ pub struct AgentEndEvent {
|
||||
pub duration_ms: Option<u64>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub num_turns: Option<u32>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub last_assistant_message: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
@@ -446,4 +473,77 @@ mod tests {
|
||||
assert!(serialized.contains("\"input_tokens\":100"));
|
||||
assert!(serialized.contains("\"output_tokens\":50"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_rate_limit_info_default() {
|
||||
let info = RateLimitInfo::default();
|
||||
assert!(info.requests_limit.is_none());
|
||||
assert!(info.requests_remaining.is_none());
|
||||
assert!(info.requests_reset.is_none());
|
||||
assert!(info.tokens_limit.is_none());
|
||||
assert!(info.tokens_remaining.is_none());
|
||||
assert!(info.tokens_reset.is_none());
|
||||
assert!(info.retry_after_ms.is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_rate_limit_event_deserialization_empty_info() {
|
||||
let json = r#"{"type":"rate_limit_event","rate_limit_info":{}}"#;
|
||||
let msg: ClaudeMessage = serde_json::from_str(json).unwrap();
|
||||
assert!(matches!(msg, ClaudeMessage::RateLimitEvent { .. }));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_rate_limit_event_deserialization_no_info() {
|
||||
// rate_limit_info field is optional via #[serde(default)]
|
||||
let json = r#"{"type":"rate_limit_event"}"#;
|
||||
let msg: ClaudeMessage = serde_json::from_str(json).unwrap();
|
||||
assert!(matches!(msg, ClaudeMessage::RateLimitEvent { .. }));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_rate_limit_event_deserialization_with_data() {
|
||||
let json = r#"{
|
||||
"type": "rate_limit_event",
|
||||
"rate_limit_info": {
|
||||
"requests_limit": 1000,
|
||||
"requests_remaining": 0,
|
||||
"requests_reset": "2024-01-01T00:01:00Z",
|
||||
"tokens_limit": 50000,
|
||||
"tokens_remaining": 0,
|
||||
"tokens_reset": "2024-01-01T00:01:00Z",
|
||||
"retry_after_ms": 60000
|
||||
}
|
||||
}"#;
|
||||
let msg: ClaudeMessage = serde_json::from_str(json).unwrap();
|
||||
if let ClaudeMessage::RateLimitEvent { rate_limit_info } = msg {
|
||||
assert_eq!(rate_limit_info.requests_limit, Some(1000));
|
||||
assert_eq!(rate_limit_info.requests_remaining, Some(0));
|
||||
assert_eq!(
|
||||
rate_limit_info.requests_reset,
|
||||
Some("2024-01-01T00:01:00Z".to_string())
|
||||
);
|
||||
assert_eq!(rate_limit_info.retry_after_ms, Some(60000));
|
||||
} else {
|
||||
panic!("Expected RateLimitEvent variant");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_rate_limit_event_ignores_unknown_fields() {
|
||||
// Ensures forward-compat: unknown fields in rate_limit_info are silently ignored
|
||||
let json = r#"{
|
||||
"type": "rate_limit_event",
|
||||
"rate_limit_info": {
|
||||
"requests_remaining": 0,
|
||||
"some_future_field": "some_value"
|
||||
}
|
||||
}"#;
|
||||
let msg: ClaudeMessage = serde_json::from_str(json).unwrap();
|
||||
if let ClaudeMessage::RateLimitEvent { rate_limit_info } = msg {
|
||||
assert_eq!(rate_limit_info.requests_remaining, Some(0));
|
||||
} else {
|
||||
panic!("Expected RateLimitEvent variant");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user