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:
+79
-8
@@ -29,6 +29,7 @@ interface StateChangePayload {
|
||||
}
|
||||
|
||||
const connectedConversations = new Set<string>();
|
||||
const greetingPendingConversations = new Set<string>();
|
||||
let unlisteners: Array<() => void> = [];
|
||||
let skipNextGreeting = false;
|
||||
|
||||
@@ -55,17 +56,17 @@ function generateGreetingPrompt(): string {
|
||||
return `[System: A new session has started. It's currently ${timeOfDay}. Please greet the user warmly and briefly. Keep it short - just 1-2 sentences.]`;
|
||||
}
|
||||
|
||||
async function sendGreeting(conversationId: string) {
|
||||
async function sendGreeting(conversationId: string): Promise<boolean> {
|
||||
// Check if we should skip this greeting
|
||||
if (skipNextGreeting) {
|
||||
skipNextGreeting = false; // Reset the flag
|
||||
return;
|
||||
return false;
|
||||
}
|
||||
|
||||
const config = configStore.getConfig();
|
||||
|
||||
if (!config.greeting_enabled) {
|
||||
return;
|
||||
return false;
|
||||
}
|
||||
|
||||
const greetingPrompt = config.greeting_custom_prompt?.trim() || generateGreetingPrompt();
|
||||
@@ -81,10 +82,12 @@ async function sendGreeting(conversationId: string) {
|
||||
conversationId,
|
||||
message: greetingPrompt,
|
||||
});
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error("Failed to send greeting:", error);
|
||||
claudeStore.addLineToConversation(conversationId, "error", `Failed to send greeting: ${error}`);
|
||||
characterState.setTemporaryState("error", 3000);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -118,6 +121,7 @@ interface WorkingDirectoryPayload {
|
||||
|
||||
export async function cleanupConversationTracking(conversationId: string) {
|
||||
connectedConversations.delete(conversationId);
|
||||
greetingPendingConversations.delete(conversationId);
|
||||
|
||||
// Clean up any temp files associated with this conversation
|
||||
try {
|
||||
@@ -173,7 +177,24 @@ export async function initializeTauriListeners() {
|
||||
if (!connectedConversations.has(targetConversationId)) {
|
||||
connectedConversations.add(targetConversationId);
|
||||
resetSessionStats(); // Reset session stats on new connection
|
||||
await sendGreeting(targetConversationId);
|
||||
|
||||
// Immediately hold the tab at yellow while we wait for the greeting response.
|
||||
// This avoids a brief green flash before the greeting is even sent.
|
||||
greetingPendingConversations.add(targetConversationId);
|
||||
claudeStore.setConnectionStatusForConversation(
|
||||
targetConversationId,
|
||||
"connecting" as ConnectionStatus
|
||||
);
|
||||
|
||||
const greetingSent = await sendGreeting(targetConversationId);
|
||||
if (!greetingSent) {
|
||||
// Greeting was disabled or failed — flip straight to connected.
|
||||
greetingPendingConversations.delete(targetConversationId);
|
||||
claudeStore.setConnectionStatusForConversation(
|
||||
targetConversationId,
|
||||
"connected" as ConnectionStatus
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if (status === "disconnected") {
|
||||
@@ -191,6 +212,7 @@ export async function initializeTauriListeners() {
|
||||
// Only remove from connected set if we're not about to reconnect
|
||||
if (!skipNextGreeting && targetConversationId) {
|
||||
connectedConversations.delete(targetConversationId);
|
||||
greetingPendingConversations.delete(targetConversationId);
|
||||
}
|
||||
|
||||
// Don't add system message if we're about to reconnect
|
||||
@@ -205,6 +227,14 @@ export async function initializeTauriListeners() {
|
||||
todos.clear();
|
||||
}
|
||||
|
||||
// Update the tab's connection status on real disconnects
|
||||
if (!skipNextGreeting && targetConversationId) {
|
||||
claudeStore.setConnectionStatusForConversation(
|
||||
targetConversationId,
|
||||
"disconnected" as ConnectionStatus
|
||||
);
|
||||
}
|
||||
|
||||
// Update character state for this conversation
|
||||
if (targetConversationId) {
|
||||
claudeStore.setCharacterStateForConversation(targetConversationId, "idle");
|
||||
@@ -214,6 +244,7 @@ export async function initializeTauriListeners() {
|
||||
|
||||
if (targetConversationId) {
|
||||
connectedConversations.delete(targetConversationId);
|
||||
greetingPendingConversations.delete(targetConversationId);
|
||||
claudeStore.addLineToConversation(targetConversationId, "error", "Connection error");
|
||||
}
|
||||
|
||||
@@ -275,11 +306,34 @@ export async function initializeTauriListeners() {
|
||||
}
|
||||
: undefined;
|
||||
|
||||
// Flip to connected when first assistant message arrives after greeting
|
||||
if (
|
||||
conversation_id &&
|
||||
line_type === "assistant" &&
|
||||
greetingPendingConversations.has(conversation_id)
|
||||
) {
|
||||
greetingPendingConversations.delete(conversation_id);
|
||||
claudeStore.setConnectionStatusForConversation(
|
||||
conversation_id,
|
||||
"connected" as ConnectionStatus
|
||||
);
|
||||
}
|
||||
|
||||
// Always store the output to the correct conversation
|
||||
if (conversation_id) {
|
||||
claudeStore.addLineToConversation(
|
||||
conversation_id,
|
||||
line_type as "user" | "assistant" | "system" | "tool" | "error" | "thinking",
|
||||
line_type as
|
||||
| "user"
|
||||
| "assistant"
|
||||
| "system"
|
||||
| "tool"
|
||||
| "error"
|
||||
| "thinking"
|
||||
| "rate-limit"
|
||||
| "compact-prompt"
|
||||
| "worktree"
|
||||
| "config-change",
|
||||
content,
|
||||
tool_name || undefined,
|
||||
costData,
|
||||
@@ -288,7 +342,17 @@ export async function initializeTauriListeners() {
|
||||
} else {
|
||||
// Fallback to active conversation if no conversation_id provided
|
||||
claudeStore.addLine(
|
||||
line_type as "user" | "assistant" | "system" | "tool" | "error" | "thinking",
|
||||
line_type as
|
||||
| "user"
|
||||
| "assistant"
|
||||
| "system"
|
||||
| "tool"
|
||||
| "error"
|
||||
| "thinking"
|
||||
| "rate-limit"
|
||||
| "compact-prompt"
|
||||
| "worktree"
|
||||
| "config-change",
|
||||
content,
|
||||
tool_name || undefined,
|
||||
costData,
|
||||
@@ -410,10 +474,17 @@ export async function initializeTauriListeners() {
|
||||
unlisteners.push(agentUpdateUnlisten);
|
||||
|
||||
const agentEndUnlisten = await listen<AgentEndPayload>("claude:agent-end", (event) => {
|
||||
const { tool_use_id, ended_at, is_error, conversation_id } = event.payload;
|
||||
const { tool_use_id, ended_at, is_error, conversation_id, last_assistant_message } =
|
||||
event.payload;
|
||||
const targetConversationId = conversation_id || get(claudeStore.activeConversationId);
|
||||
if (targetConversationId) {
|
||||
agentStore.endAgent(targetConversationId, tool_use_id, ended_at, is_error);
|
||||
agentStore.endAgent(
|
||||
targetConversationId,
|
||||
tool_use_id,
|
||||
ended_at,
|
||||
is_error,
|
||||
last_assistant_message
|
||||
);
|
||||
}
|
||||
});
|
||||
unlisteners.push(agentEndUnlisten);
|
||||
|
||||
Reference in New Issue
Block a user