Files
hikari-desktop/src/lib/utils/conversationUtils.ts
T
naomi 1c45507cdf
Security Scan and Upload / Security & DefectDojo Upload (push) Successful in 1m1s
CI / Lint & Test (push) Has been cancelled
CI / Build Linux (push) Has been cancelled
CI / Build Windows (cross-compile) (push) Has been cancelled
feat: massive overhaul to manage costs (#103)
### Explanation

_No response_

### Issue

Closes #102

### 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: #103
Co-authored-by: Naomi Carrigan <commits@nhcarrigan.com>
Co-committed-by: Naomi Carrigan <commits@nhcarrigan.com>
2026-02-04 19:58:43 -08:00

111 lines
3.6 KiB
TypeScript

import type { ConversationSummary } from "$lib/stores/conversations";
/**
* Sanitises a string for safe JSON serialization through Tauri IPC.
* Removes control characters and lone surrogates that could cause issues
* during JSON serialization/deserialization.
*/
export function sanitizeForJson(text: string): string {
// Remove control characters except for common whitespace (tab, newline, carriage return)
// These can cause JSON parsing issues and are rarely meaningful in conversation summaries.
// eslint-disable-next-line no-control-regex -- regex uses control character codes
let sanitized = text.replace(/[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]/g, "");
// Remove extended ASCII control chars (C1 control codes)
sanitized = sanitized.replace(/[\x80-\x9F]/g, "");
// Remove lone surrogates (U+D800 to U+DFFF) which cause "unexpected end of hex escape"
// errors in serde_json when they appear without proper pairing.
// These are invalid in JSON and can cause parse failures.
sanitized = sanitized.replace(/[\uD800-\uDFFF]/g, "");
return sanitized;
}
/**
* Generates a prompt to ask Claude to summarise a conversation.
* This can be sent as a user message to get a summary.
*/
export function generateSummaryPrompt(conversationContent: string): string {
return `Please provide a concise summary of our conversation so far. Focus on:
1. Key topics discussed
2. Important decisions or conclusions made
3. Any ongoing tasks or context that would be helpful to remember
4. Code changes or files that were modified
Keep the summary brief but comprehensive enough to continue our work in a new session.
Here is our conversation:
${conversationContent}
Please provide the summary now:`;
}
/**
* Generates a context injection message to prepend to a new conversation.
* This provides Claude with context from a previous session.
*/
export function generateContextInjection(summary: ConversationSummary): string {
return `[Previous Session Context]
The following is a summary from our previous conversation (${summary.messageCount} messages, approximately ${summary.tokenEstimate.toLocaleString()} tokens):
${summary.content}
[End of Previous Context]
Please continue from where we left off, or let me know if you need any clarification about the previous context.`;
}
/**
* Estimates the token count for a given string.
* Uses a rough approximation of ~4 characters per token.
*/
export function estimateTokens(text: string): number {
return Math.ceil(text.length / 4);
}
/**
* Creates a ConversationSummary object from summary content.
*/
export function createSummary(
content: string,
messageCount: number,
originalTokenEstimate: number
): ConversationSummary {
return {
generatedAt: new Date(),
content,
messageCount,
tokenEstimate: originalTokenEstimate,
};
}
/**
* Determines if a conversation should be compacted based on token usage.
* Returns true if the conversation is using more than the threshold percentage
* of the context window.
*/
export function shouldSuggestCompaction(
contextTokensUsed: number,
contextWindowLimit: number,
thresholdPercent: number = 60
): boolean {
if (contextWindowLimit === 0) return false;
const utilisationPercent = (contextTokensUsed / contextWindowLimit) * 100;
return utilisationPercent >= thresholdPercent;
}
/**
* Formats a token count for display.
*/
export function formatTokenCount(tokens: number): string {
if (tokens >= 1000000) {
return `${(tokens / 1000000).toFixed(1)}M`;
}
if (tokens >= 1000) {
return `${(tokens / 1000).toFixed(1)}K`;
}
return tokens.toString();
}