feat: add ability to run multiple agents via tabbed views (#47)
Security Scan and Upload / Security & DefectDojo Upload (push) Successful in 54s
CI / Lint & Test (push) Successful in 14m18s
CI / Build Linux (push) Successful in 16m46s
CI / Build Windows (cross-compile) (push) Successful in 26m39s

### Explanation

_No response_

### Issue

Closes #30 Closes #41

### Attestations

- [ ] I have read and agree to the [Code of Conduct](https://docs.nhcarrigan.com/community/coc/)
- [ ] I have read and agree to the [Community Guidelines](https://docs.nhcarrigan.com/community/guide/).
- [ ] My contribution complies with the [Contributor Covenant](https://docs.nhcarrigan.com/dev/covenant/).

### Dependencies

- [ ] I have pinned the dependencies to a specific patch version.

### Style

- [ ] I have run the linter and resolved any errors.
- [ ] My pull request uses an appropriate title, matching the conventional commit standards.
- [ ] My scope of feat/fix/chore/etc. correctly matches the nature of changes in my pull request.

### Tests

- [ ] My contribution adds new code, and I have added tests to cover it.
- [ ] My contribution modifies existing code, and I have updated the tests to reflect these changes.
- [ ] All new and existing tests pass locally with my changes.
- [ ] Code coverage remains at or above the configured threshold.

### Documentation

_No response_

### Versioning

_No response_

Reviewed-on: #47
Co-authored-by: Naomi Carrigan <commits@nhcarrigan.com>
Co-committed-by: Naomi Carrigan <commits@nhcarrigan.com>
This commit was merged in pull request #47.
This commit is contained in:
2026-01-20 13:57:48 -08:00
committed by Naomi Carrigan
parent 2d3adcab1c
commit d83697e5cf
20 changed files with 1375 additions and 287 deletions
+74 -135
View File
@@ -1,147 +1,86 @@
import { writable, derived } from "svelte/store";
import type { ConnectionStatus, PermissionRequest } from "$lib/types/messages";
import { derived } from "svelte/store";
import { conversationsStore } from "./conversations";
import type { TerminalLine } from "$lib/types/messages";
import { characterState } from "$lib/stores/character";
import {
setShouldRestoreHistory,
setSavedHistory,
getShouldRestoreHistory,
getSavedHistory,
clearHistoryRestore,
} from "./historyRestore";
export interface TerminalLine {
id: string;
type: "user" | "assistant" | "system" | "tool" | "error";
content: string;
timestamp: Date;
toolName?: string;
}
// Re-export TerminalLine type for backwards compatibility
export type { TerminalLine };
function createClaudeStore() {
const connectionStatus = writable<ConnectionStatus>("disconnected");
const sessionId = writable<string | null>(null);
const currentWorkingDirectory = writable<string>("");
const terminalLines = writable<TerminalLine[]>([]);
const pendingPermission = writable<PermissionRequest | null>(null);
const isProcessing = writable<boolean>(false);
const grantedTools = writable<Set<string>>(new Set());
const pendingRetryMessage = writable<string | null>(null);
const shouldRestoreHistory = writable<boolean>(false);
const savedConversationHistory = writable<string | null>(null);
// Re-export from conversations store for backwards compatibility
export const claudeStore = {
// Existing subscriptions
connectionStatus: conversationsStore.connectionStatus,
sessionId: conversationsStore.sessionId,
currentWorkingDirectory: conversationsStore.currentWorkingDirectory,
terminalLines: conversationsStore.terminalLines,
pendingPermission: conversationsStore.pendingPermission,
isProcessing: conversationsStore.isProcessing,
grantedTools: conversationsStore.grantedTools,
pendingRetryMessage: conversationsStore.pendingRetryMessage,
let lineIdCounter = 0;
// New conversation-aware subscriptions
conversations: conversationsStore.conversations,
activeConversationId: conversationsStore.activeConversationId,
activeConversation: conversationsStore.activeConversation,
function generateLineId(): string {
return `line-${Date.now()}-${lineIdCounter++}`;
}
// Methods
setConnectionStatus: conversationsStore.setConnectionStatus,
setConnectionStatusForConversation: conversationsStore.setConnectionStatusForConversation,
setCharacterStateForConversation: conversationsStore.setCharacterStateForConversation,
setSessionId: conversationsStore.setSessionId,
setSessionIdForConversation: conversationsStore.setSessionIdForConversation,
setWorkingDirectory: conversationsStore.setWorkingDirectory,
setWorkingDirectoryForConversation: conversationsStore.setWorkingDirectoryForConversation,
setProcessing: conversationsStore.setProcessing,
addLine: conversationsStore.addLine,
addLineToConversation: conversationsStore.addLineToConversation,
updateLine: conversationsStore.updateLine,
appendToLine: conversationsStore.appendToLine,
clearTerminal: conversationsStore.clearTerminal,
getConversationHistory: conversationsStore.getConversationHistory,
requestPermission: conversationsStore.requestPermission,
clearPermission: conversationsStore.clearPermission,
grantTool: conversationsStore.grantTool,
revokeAllTools: conversationsStore.revokeAllTools,
isToolGranted: conversationsStore.isToolGranted,
setPendingRetryMessage: conversationsStore.setPendingRetryMessage,
return {
connectionStatus: { subscribe: connectionStatus.subscribe },
sessionId: { subscribe: sessionId.subscribe },
currentWorkingDirectory: { subscribe: currentWorkingDirectory.subscribe },
terminalLines: { subscribe: terminalLines.subscribe },
pendingPermission: { subscribe: pendingPermission.subscribe },
isProcessing: { subscribe: isProcessing.subscribe },
grantedTools: { subscribe: grantedTools.subscribe },
pendingRetryMessage: { subscribe: pendingRetryMessage.subscribe },
shouldRestoreHistory: { subscribe: shouldRestoreHistory.subscribe },
savedConversationHistory: { subscribe: savedConversationHistory.subscribe },
// Conversation management
createConversation: conversationsStore.createConversation,
deleteConversation: conversationsStore.deleteConversation,
switchConversation: conversationsStore.switchConversation,
renameConversation: conversationsStore.renameConversation,
setConnectionStatus: (status: ConnectionStatus) => connectionStatus.set(status),
setSessionId: (id: string | null) => sessionId.set(id),
setWorkingDirectory: (dir: string) => currentWorkingDirectory.set(dir),
setProcessing: (processing: boolean) => isProcessing.set(processing),
getGrantedTools: (): string[] => {
let tools: string[] = [];
conversationsStore.grantedTools.subscribe((t) => (tools = Array.from(t)))();
return tools;
},
addLine: (type: TerminalLine["type"], content: string, toolName?: string) => {
const line: TerminalLine = {
id: generateLineId(),
type,
content,
timestamp: new Date(),
toolName,
};
terminalLines.update((lines) => [...lines, line]);
return line.id;
},
// History restoration methods from main branch
setShouldRestoreHistory: setShouldRestoreHistory,
setSavedConversationHistory: setSavedHistory,
getShouldRestoreHistory: getShouldRestoreHistory,
getSavedConversationHistory: getSavedHistory,
updateLine: (id: string, content: string) => {
terminalLines.update((lines) =>
lines.map((line) => (line.id === id ? { ...line, content } : line))
);
},
appendToLine: (id: string, additionalContent: string) => {
terminalLines.update((lines) =>
lines.map((line) =>
line.id === id ? { ...line, content: line.content + additionalContent } : line
)
);
},
clearTerminal: () => terminalLines.set([]),
getConversationHistory: (): string => {
let lines: TerminalLine[] = [];
terminalLines.subscribe((l) => (lines = l))();
// Filter to just user and assistant messages, skip system/tool noise
const relevantLines = lines.filter(
(line) => line.type === "user" || line.type === "assistant"
);
if (relevantLines.length === 0) return "";
return relevantLines
.map((line) => {
const role = line.type === "user" ? "User" : "Assistant";
return `${role}: ${line.content}`;
})
.join("\n\n");
},
requestPermission: (request: PermissionRequest) => pendingPermission.set(request),
clearPermission: () => pendingPermission.set(null),
grantTool: (toolName: string) => {
grantedTools.update((tools) => {
const newTools = new Set(tools);
newTools.add(toolName);
return newTools;
});
},
getGrantedTools: (): string[] => {
let tools: string[] = [];
grantedTools.subscribe((t) => (tools = Array.from(t)))();
return tools;
},
setPendingRetryMessage: (message: string | null) => pendingRetryMessage.set(message),
setShouldRestoreHistory: (should: boolean) => shouldRestoreHistory.set(should),
setSavedConversationHistory: (history: string | null) => savedConversationHistory.set(history),
getShouldRestoreHistory: (): boolean => {
let should = false;
shouldRestoreHistory.subscribe((s) => (should = s))();
return should;
},
getSavedConversationHistory: (): string | null => {
let history: string | null = null;
savedConversationHistory.subscribe((h) => (history = h))();
return history;
},
reset: () => {
connectionStatus.set("disconnected");
sessionId.set(null);
currentWorkingDirectory.set("");
terminalLines.set([]);
pendingPermission.set(null);
isProcessing.set(false);
grantedTools.set(new Set());
pendingRetryMessage.set(null);
shouldRestoreHistory.set(false);
savedConversationHistory.set(null);
},
};
}
export const claudeStore = createClaudeStore();
reset: () => {
// Reset only the active conversation
conversationsStore.clearTerminal();
conversationsStore.setSessionId(null);
conversationsStore.setWorkingDirectory("");
conversationsStore.setProcessing(false);
conversationsStore.revokeAllTools();
// Also clear history restoration
clearHistoryRestore();
},
};
export const hasPermissionPending = derived(
claudeStore.pendingPermission,