Merge main into coding-style-fixes, resolving conflicts

Resolved conflicts in favor of main's nullish coalescing (??) and
AI SDK v6 reasoning-start/reasoning-end streaming approach.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Mike Thicke 2026-02-15 08:36:52 -05:00
commit 5666bc51c4
13 changed files with 629 additions and 565 deletions

3
.gitignore vendored
View file

@ -135,6 +135,9 @@ dist
.yarn/install-state.gz
.pnp.*
# Test vault chat files
test-vault/coi/chat_*.md
#macos
.DS_Store

View file

@ -107,6 +107,35 @@ I am an idependent software developer. If you find Co-Intelligence to be useful,
[<img style='border:0px;height:36px;' src='https://storage.ko-fi.com/cdn/kofi6.png?v=6' border='0' alt='Buy Me a Coffee at ko-fi.com' />](https://ko-fi.com/X8X71G7YSI)
## Development
### Setup
```bash
npm install
npm run dev # Watch mode — rebuilds on file changes
npm run build # Production build (also validates TypeScript types)
npm test # Run tests with Vitest
```
### Manual Testing
A test vault is included at `test-vault/` so you can test the plugin in Obsidian without affecting your personal vault. To build and open it:
```bash
./scripts/open-test-vault.sh
```
The first time you open the vault, you'll need to enable community plugins in Settings > Community plugins. After that, the Co-Intelligence AI plugin will be pre-enabled.
The script symlinks the build output (`dist/`) into the test vault's plugin directory, so rebuilds are picked up automatically. For a live development workflow:
1. Run `npm run dev` in one terminal
2. Run `./scripts/open-test-vault.sh` to open the vault
3. After changes rebuild, reload Obsidian (Cmd/Ctrl+P > "Reload app without saving")
The test vault includes sample folders and system prompts for exercising plugin features.
## Contributing, Feedback, and Help
This is an open source project, using the [MIT License](LICENSE). Pull requests for bug fixes or small improvements are welcome. If you want to get involved in a more substantial way, please [Contact me](https://epistemic.technology/contact/).

View file

@ -14,18 +14,20 @@
"author": "Epistemic Technology",
"license": "MIT",
"dependencies": {
"@ai-sdk/anthropic": "^3.0.37",
"@ai-sdk/google": "^3.0.21",
"@ai-sdk/openai": "^3.0.25",
"@ai-sdk/perplexity": "^3.0.17",
"ai": "^6.0.73",
"@ai-sdk/anthropic": "^3.0.41",
"@ai-sdk/google": "^3.0.24",
"@ai-sdk/openai": "^3.0.26",
"@ai-sdk/perplexity": "^3.0.18",
"ai": "^6.0.79",
"monkey-around": "^2.3.0",
"solid-js": "^1.9.11",
"zod": "^3.25.76"
},
"pnpm": {
"overrides": {
"vitest>vite": "^6.4.1"
"vitest>vite": "^6.4.1",
"seroval": "^1.4.1",
"jsondiffpatch": "^0.7.2"
}
},
"devDependencies": {

File diff suppressed because it is too large Load diff

48
scripts/open-test-vault.sh Executable file
View file

@ -0,0 +1,48 @@
#!/usr/bin/env bash
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
PROJECT_DIR="$(dirname "$SCRIPT_DIR")"
VAULT_DIR="$PROJECT_DIR/test-vault"
PLUGIN_DIR="$VAULT_DIR/.obsidian/plugins/co-intelligence"
OBSIDIAN_CONFIG="$HOME/Library/Application Support/obsidian/obsidian.json"
# Create plugin directory and symlinks
mkdir -p "$PLUGIN_DIR"
ln -sf "$PROJECT_DIR/dist/main.js" "$PLUGIN_DIR/main.js"
ln -sf "$PROJECT_DIR/dist/styles.css" "$PLUGIN_DIR/styles.css"
ln -sf "$PROJECT_DIR/manifest.json" "$PLUGIN_DIR/manifest.json"
# Build latest
echo "Building plugin..."
npm --prefix "$PROJECT_DIR" run build
# Register the vault with Obsidian if not already registered
if [ -f "$OBSIDIAN_CONFIG" ]; then
if ! python3 -c "
import json, sys
with open('$OBSIDIAN_CONFIG') as f:
config = json.load(f)
sys.exit(0 if any(v.get('path') == '$VAULT_DIR' for v in config.get('vaults', {}).values()) else 1)
"; then
echo "Registering test vault with Obsidian..."
python3 -c "
import json, hashlib, time
config_path = '$OBSIDIAN_CONFIG'
vault_path = '$VAULT_DIR'
with open(config_path) as f:
config = json.load(f)
vault_id = hashlib.md5(vault_path.encode()).hexdigest()[:16]
config.setdefault('vaults', {})[vault_id] = {'path': vault_path, 'ts': int(time.time() * 1000)}
with open(config_path, 'w') as f:
json.dump(config, f)
"
fi
fi
# Open the test vault in Obsidian
echo "Opening test vault in Obsidian..."
ENCODED_PATH=$(python3 -c "import urllib.parse; print(urllib.parse.quote('$VAULT_DIR'))")
open "obsidian://open?path=$ENCODED_PATH"
echo "Done. If this is the first time, enable the plugin in Settings > Community plugins."

View file

@ -14,7 +14,7 @@ export const BotMessage: Component<BotMessageProps> = ({
}: BotMessageProps) => {
const file = useContext(FileContext);
const filePath = file?.path || "";
const content = (message.content as string) || "";
const content = (message.content as string) ?? "";
// Perplexity models use <think> tags to indicate the reasoning section.
// OpenAI models use chunk types instead. These are translated into <think>
// tags by handleSendMessage in ChatInterface.tsx

View file

@ -176,34 +176,31 @@ export const ChatInterface = ({
console.error("Error:", chunk.error);
new Notice("Unknown error occurred. See console log for details.");
}
if (
chunk.type !== "text-delta" &&
chunk.type !== "reasoning-delta"
) {
if (chunk.type === "reasoning-start") {
doingReasoning = true;
accumulatedContent += "<think>";
continue;
}
if (!chunk.text) {
if (chunk.type === "reasoning-end") {
doingReasoning = false;
accumulatedContent += "</think>";
continue;
}
if (chunk.type !== "text-delta" && chunk.type !== "reasoning-delta") {
continue;
}
const text = chunk.text;
if (isFirstChunk) {
const assistantMessage: ModelChatMessage = {
role: "assistant",
content: chunk.text,
content: text,
};
setMessages([...messages(), assistantMessage]);
if (chunk.type === "reasoning-delta") {
accumulatedContent = "<think>";
doingReasoning = true;
}
accumulatedContent += chunk.text;
accumulatedContent += text;
isFirstChunk = false;
setIsProcessing(false);
} else {
if (doingReasoning && chunk.type !== "reasoning-delta") {
accumulatedContent += "</think>";
doingReasoning = false;
}
accumulatedContent += chunk.text;
accumulatedContent += text;
setMessages((prevMessages) => {
const updatedMessages = [...prevMessages];
updatedMessages[updatedMessages.length - 1] = {
@ -252,7 +249,7 @@ export const ChatInterface = ({
// Replace source reference numbers [n] with [n+offset]
const offset = lastSourceLinkNumber();
const updatedContent = (lastMessage.content as string).replace(
const updatedContent = ((lastMessage.content as string) ?? "").replace(
/\[(\d+)\]/g,
(match, num) => {
const source = uniqueNewSources[parseInt(num) - 1];
@ -280,7 +277,9 @@ export const ChatInterface = ({
if (!hasProcessedSources) {
const currentMessage = messages()[messages().length - 1];
const newLinks =
(currentMessage.content as string).match(/\[(.*?)\]\((.*?)\)/g) || [];
((currentMessage.content as string) ?? "").match(
/\[(.*?)\]\((.*?)\)/g,
) || [];
newLinks.forEach((link) => {
const [text, url] = link.slice(1, -1).split("](");
const existingSource = sources().find((source) => source.url === url);

View file

@ -182,7 +182,7 @@ export function serializeCoiNoteContent(
): string {
const serializedMessages = messages
.map(({ role, content }) => {
const contentStr = (content as string) || "";
const contentStr = (content as string) ?? "";
// Find the highest level header in the content
const headerMatches = contentStr.match(/^#+/gm);

7
test-vault/.gitignore vendored Normal file
View file

@ -0,0 +1,7 @@
# Obsidian generates config files when the vault is opened.
# Only community-plugins.json is checked in (to pre-enable the plugin).
.obsidian/*
!.obsidian/community-plugins.json
# Plugin symlinks are machine-specific; the script recreates them.
.obsidian/plugins/

View file

@ -0,0 +1,3 @@
[
"co-intelligence"
]

View file

@ -0,0 +1 @@
You are a code reviewer. Review code for bugs, style issues, and potential improvements. Be constructive and specific.

View file

@ -0,0 +1 @@
You are a helpful assistant. Be concise and clear in your responses.

View file

@ -0,0 +1,13 @@
This is a sample note for testing context linking in Co-Intelligence AI.
## Key points
- First point with **bold** and *italic* formatting
- Second point with a `code snippet`
- Third point with a [[link to nowhere]]
```javascript
function hello() {
console.log("Hello from a code block");
}
```