generated from nhcarrigan/template
feat: consume worktree field from status line hook events
Parse structured WorktreeInfo (name, path, branch, original_repo_directory) from WorktreeCreate/Remove hook events and emit a dedicated claude:worktree event. Store per-conversation worktree state and display an emerald branch badge in the status bar so users can see at a glance which worktree and branch each session is running on. Closes #206
This commit is contained in:
@@ -29,6 +29,7 @@
|
||||
|
||||
let connectionStatus: ConnectionStatus = $state("disconnected");
|
||||
let workingDirectory = $state("");
|
||||
let worktreeInfo: import("$lib/types/worktree").WorktreeInfo | null = $state(null);
|
||||
let selectedDirectory = $state("/home/naomi");
|
||||
let isConnecting = $state(false);
|
||||
let grantedToolsList: string[] = $state([]);
|
||||
@@ -115,6 +116,10 @@
|
||||
workingDirectory = dir;
|
||||
});
|
||||
|
||||
claudeStore.worktreeInfo.subscribe((info) => {
|
||||
worktreeInfo = info;
|
||||
});
|
||||
|
||||
claudeStore.grantedTools.subscribe((tools) => {
|
||||
grantedToolsList = Array.from(tools);
|
||||
});
|
||||
@@ -392,6 +397,18 @@
|
||||
{workingDirectory}
|
||||
</div>
|
||||
{/if}
|
||||
{#if worktreeInfo}
|
||||
<div
|
||||
class="flex items-center gap-1 px-2 py-0.5 rounded-full bg-emerald-500/15 border border-emerald-500/30 text-emerald-400 text-xs"
|
||||
title="Worktree: {worktreeInfo.name} | Base: {worktreeInfo.original_repo_directory}"
|
||||
>
|
||||
<svg class="w-3 h-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
||||
d="M13 10V3L4 14h7v7l9-11h-7z" />
|
||||
</svg>
|
||||
{worktreeInfo.branch}
|
||||
</div>
|
||||
{/if}
|
||||
{:else}
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="text-sm text-gray-600">cwd:</span>
|
||||
|
||||
@@ -26,6 +26,7 @@ export const claudeStore = {
|
||||
grantedTools: conversationsStore.grantedTools,
|
||||
pendingRetryMessage: conversationsStore.pendingRetryMessage,
|
||||
attachments: conversationsStore.attachments,
|
||||
worktreeInfo: conversationsStore.worktreeInfo,
|
||||
|
||||
// New conversation-aware subscriptions
|
||||
conversations: conversationsStore.conversations,
|
||||
@@ -70,6 +71,9 @@ export const claudeStore = {
|
||||
// Draft text (per-tab input persistence)
|
||||
setDraftText: conversationsStore.setDraftText,
|
||||
|
||||
// Worktree info (per-conversation)
|
||||
setWorktreeInfo: conversationsStore.setWorktreeInfo,
|
||||
|
||||
// Conversation management
|
||||
createConversation: conversationsStore.createConversation,
|
||||
deleteConversation: conversationsStore.deleteConversation,
|
||||
|
||||
@@ -562,6 +562,54 @@ describe("draft text persistence", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("worktreeInfo state management", () => {
|
||||
it("initialises worktreeInfo as null", () => {
|
||||
const conversation = { worktreeInfo: null };
|
||||
expect(conversation.worktreeInfo).toBeNull();
|
||||
});
|
||||
|
||||
it("stores worktreeInfo when a worktree is created", () => {
|
||||
const info = {
|
||||
name: "worktree-abc",
|
||||
path: "/tmp/worktrees/worktree-abc",
|
||||
branch: "feat/my-feature",
|
||||
original_repo_directory: "/home/naomi/code/project",
|
||||
};
|
||||
const conversation = { worktreeInfo: null as typeof info | null };
|
||||
conversation.worktreeInfo = info;
|
||||
|
||||
expect(conversation.worktreeInfo?.branch).toBe("feat/my-feature");
|
||||
expect(conversation.worktreeInfo?.name).toBe("worktree-abc");
|
||||
expect(conversation.worktreeInfo?.original_repo_directory).toBe("/home/naomi/code/project");
|
||||
});
|
||||
|
||||
it("clears worktreeInfo when a worktree is removed", () => {
|
||||
const info = {
|
||||
name: "worktree-abc",
|
||||
path: "/tmp/worktrees/worktree-abc",
|
||||
branch: "feat/my-feature",
|
||||
original_repo_directory: "/home/naomi/code/project",
|
||||
};
|
||||
const conversation = { worktreeInfo: info as typeof info | null };
|
||||
conversation.worktreeInfo = null;
|
||||
|
||||
expect(conversation.worktreeInfo).toBeNull();
|
||||
});
|
||||
|
||||
it("stores worktreeInfo independently per conversation", () => {
|
||||
const conversations = new Map([
|
||||
["conv-1", { worktreeInfo: null as { branch: string } | null }],
|
||||
["conv-2", { worktreeInfo: null as { branch: string } | null }],
|
||||
]);
|
||||
|
||||
const conv1 = conversations.get("conv-1");
|
||||
if (conv1) conv1.worktreeInfo = { branch: "feat/one" };
|
||||
|
||||
expect(conversations.get("conv-1")?.worktreeInfo?.branch).toBe("feat/one");
|
||||
expect(conversations.get("conv-2")?.worktreeInfo).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe("isProcessing state management", () => {
|
||||
it("starts as false by default", () => {
|
||||
const conversation = { id: "conv-1", isProcessing: false };
|
||||
|
||||
@@ -7,6 +7,7 @@ import type {
|
||||
Attachment,
|
||||
} from "$lib/types/messages";
|
||||
import type { CharacterState } from "$lib/types/states";
|
||||
import type { WorktreeInfo } from "$lib/types/worktree";
|
||||
import { cleanupConversationTracking } from "$lib/tauri";
|
||||
import { characterState } from "$lib/stores/character";
|
||||
import { sessionsStore } from "$lib/stores/sessions";
|
||||
@@ -41,6 +42,7 @@ export interface Conversation {
|
||||
successSoundFired: boolean;
|
||||
taskStartSoundFired: boolean;
|
||||
draftText: string;
|
||||
worktreeInfo: WorktreeInfo | null;
|
||||
}
|
||||
|
||||
const TAB_NAMES = [
|
||||
@@ -165,6 +167,7 @@ function createConversationsStore() {
|
||||
successSoundFired: false,
|
||||
taskStartSoundFired: false,
|
||||
draftText: "",
|
||||
worktreeInfo: null,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -220,6 +223,7 @@ function createConversationsStore() {
|
||||
const pendingQuestion = derived(activeConversation, ($conv) => $conv?.pendingQuestion || null);
|
||||
const scrollPosition = derived(activeConversation, ($conv) => $conv?.scrollPosition ?? -1);
|
||||
const attachments = derived(activeConversation, ($conv) => $conv?.attachments || []);
|
||||
const worktreeInfo = derived(activeConversation, ($conv) => $conv?.worktreeInfo ?? null);
|
||||
|
||||
return {
|
||||
// Expose derived stores for compatibility
|
||||
@@ -235,6 +239,7 @@ function createConversationsStore() {
|
||||
pendingRetryMessage: { subscribe: pendingRetryMessage.subscribe },
|
||||
scrollPosition: { subscribe: scrollPosition.subscribe },
|
||||
attachments: { subscribe: attachments.subscribe },
|
||||
worktreeInfo: { subscribe: worktreeInfo.subscribe },
|
||||
|
||||
// New conversation-specific stores
|
||||
conversations: { subscribe: conversations.subscribe },
|
||||
@@ -976,6 +981,16 @@ function createConversationsStore() {
|
||||
});
|
||||
},
|
||||
|
||||
setWorktreeInfo: (conversationId: string, info: WorktreeInfo | null) => {
|
||||
conversations.update((convs) => {
|
||||
const conv = convs.get(conversationId);
|
||||
if (conv) {
|
||||
conv.worktreeInfo = info;
|
||||
}
|
||||
return convs;
|
||||
});
|
||||
},
|
||||
|
||||
// Add initialization helper
|
||||
initialize: () => {
|
||||
ensureInitialized();
|
||||
|
||||
@@ -13,6 +13,7 @@ import type {
|
||||
} from "$lib/types/messages";
|
||||
import type { CharacterState } from "$lib/types/states";
|
||||
import type { AgentStartPayload, AgentEndPayload } from "$lib/types/agents";
|
||||
import type { WorktreeEvent } from "$lib/types/worktree";
|
||||
import { agentStore } from "$lib/stores/agents";
|
||||
import { todos } from "$lib/stores/todos";
|
||||
import {
|
||||
@@ -563,6 +564,18 @@ export async function initializeTauriListeners() {
|
||||
});
|
||||
unlisteners.push(agentEndUnlisten);
|
||||
|
||||
const worktreeUnlisten = await listen<WorktreeEvent>("claude:worktree", (event) => {
|
||||
const { conversation_id, event_type, worktree } = event.payload;
|
||||
const targetConversationId = conversation_id || get(claudeStore.activeConversationId);
|
||||
if (targetConversationId) {
|
||||
claudeStore.setWorktreeInfo(
|
||||
targetConversationId,
|
||||
event_type === "create" && worktree ? worktree : null
|
||||
);
|
||||
}
|
||||
});
|
||||
unlisteners.push(worktreeUnlisten);
|
||||
|
||||
const questionUnlisten = await listen<UserQuestionEvent>("claude:question", (event) => {
|
||||
const questionEvent = event.payload;
|
||||
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
export interface WorktreeInfo {
|
||||
name: string;
|
||||
path: string;
|
||||
branch: string;
|
||||
original_repo_directory: string;
|
||||
}
|
||||
|
||||
export interface WorktreeEvent {
|
||||
conversation_id?: string;
|
||||
/** "create" or "remove" */
|
||||
event_type: string;
|
||||
worktree?: WorktreeInfo;
|
||||
}
|
||||
Reference in New Issue
Block a user