Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
61 commits
Select commit Hold shift + click to select a range
92d7d1b
Merge remote-tracking branch 'origin/fix/workspace-reindex-storm' int…
Szowesgad Jun 3, 2026
3c1600a
Merge remote-tracking branch 'origin/feat/unicode-capabilities' into …
Szowesgad Jun 4, 2026
b55bf61
[codex/interactive] chore: Polish Makefile help output formatting
Szowesgad Jun 4, 2026
f9bd681
[claude/interactive] fix: restore native toolbar, Replace, Share + ta…
Szowesgad Jun 4, 2026
2a1ffca
[claude/interactive] chore: track docs/specs, add per-window tabs plan
Szowesgad Jun 5, 2026
b66415b
[claude/interactive] chore: stop tracking internal docs; restore broa…
Szowesgad Jun 5, 2026
a152d00
Fix native document tab routing
Szowesgad Jun 5, 2026
9646193
fix: floating format bar keeps focus + move IndexDatabase off main (P0)
Szowesgad Jun 5, 2026
1965a33
Add install-app target and bump to 0.2.3
Szowesgad Jun 5, 2026
1e181dd
[claude/claude] fix(index): move autosave single-doc index off the ma…
Szowesgad Jun 6, 2026
4cda581
[claude/interactive] fix(editor): stop per-keystroke scroll jump + re…
Szowesgad Jun 6, 2026
dc8961e
[claude/interactive] fix(workspace): move watcher-refresh tree rebuil…
Szowesgad Jun 6, 2026
97f224a
[codex/unknown] fix(window-tabs): defer modal-time native tab mutation
Szowesgad Jun 6, 2026
eff2f89
[claude/interactive] fix(tests): balance NSWindow lifetime in modal-t…
Szowesgad Jun 6, 2026
5e1f458
marble: harden modal-time document window opens
Szowesgad Jun 6, 2026
66d15e3
[junie/interactive] fix(workspace): harden persisted cache file prote…
Szowesgad Jun 7, 2026
a3f64a5
[junie/gpt-5.4] test(indexing): harden autosave reindex assertions
Szowesgad Jun 7, 2026
208648a
[codex/codex] feat(editor): add rich markdown typing autoconversion
Szowesgad Jun 7, 2026
1a3164a
[codex/codex] feat(markdown): render wikilinks in preview
Szowesgad Jun 7, 2026
c7ed51f
[codex/codex] feat(preview): sync editor and preview scroll
Szowesgad Jun 7, 2026
e970c8a
[codex/unknown] feat(preview): render math delimiters in preview
Szowesgad Jun 7, 2026
2e765c8
[codex/unknown] feat(editor): center caret in focus mode
Szowesgad Jun 7, 2026
1ccee1d
[codex/unknown] feat(export): add html and pdf document export
Szowesgad Jun 7, 2026
825cfa4
[codex/codex] build(ffi): link qube bridge into Pensieve
Szowesgad Jun 7, 2026
96b2937
[codex/codex] feat(transcription): add Vista listener accumulation se…
Szowesgad Jun 7, 2026
f21c499
[codex/codex] feat(backlinks): add indexed wikilink backlink engine
Szowesgad Jun 7, 2026
f3cfa3c
[codex/codex] feat(editor): close inline highlight autoconversion
Szowesgad Jun 7, 2026
bbdb719
[claude/interactive] fix(editor): break preview→editor scroll-sync fe…
Szowesgad Jun 7, 2026
c4a0304
[codex/gpt-5] feat(storage): add crash-safe untitled recovery
Szowesgad Jun 8, 2026
eb81105
[codex/gpt-5] fix(workspace): honor gitignore during scans
Szowesgad Jun 8, 2026
567db17
[claude/interactive] fix(release): embed + re-sign qube-ffi dylib so …
Szowesgad Jun 8, 2026
94a238c
docs: add GitHub Pages landing, SECURITY.md, and CI workflow
Szowesgad Jun 8, 2026
1c76194
[codex/gpt-5] fix(window): reap empty launcher windows after document…
Szowesgad Jun 8, 2026
78c381c
docs: enrich SECURITY.md and add .nojekyll for GitHub Pages
Szowesgad Jun 8, 2026
06eb730
fix(app): drive native document window titles by registry
Szowesgad Jun 8, 2026
5aacb55
fix(preview): update WKWebView content without full reload
Szowesgad Jun 8, 2026
f709e47
fix(launch): stabilize first window paint
Szowesgad Jun 8, 2026
cf6c87b
fix(tabs): preserve single-window tab bar state
Szowesgad Jun 8, 2026
e7ae54e
test(backlinks): cover wikilink backlink resolution
Szowesgad Jun 8, 2026
35f9297
fix: keep workspace cache writes from aborting stats tests
Szowesgad Jun 8, 2026
fa3ab82
test: harden transcription accumulation buffering
Szowesgad Jun 8, 2026
23a3e28
[claude/interactive] chore(debug): surface per-window session state i…
Szowesgad Jun 9, 2026
32a1a3a
[claude/interactive] feat(a11y): accessibility identifiers on editor …
Szowesgad Jun 9, 2026
3354808
[claude/interactive] chore(debug): remove temp title-diagnostic subtitle
Szowesgad Jun 9, 2026
d4aa124
[claude/interactive] fix(editor): break re-entrant editor<->preview s…
Szowesgad Jun 9, 2026
77c9def
test(backlinks): verify indexed target title lookup
Szowesgad Jun 9, 2026
5f74a15
fix(tabs): expose native tab bar toggle
Szowesgad Jun 9, 2026
cfce9ed
fix(launch): stabilize first window paint
Szowesgad Jun 9, 2026
3e6e8a7
test(index): harden stats lookup failures
Szowesgad Jun 9, 2026
b978641
fix(launch): align startup presentation modifiers
Szowesgad Jun 9, 2026
9412278
[codex/justdo] fix(app): reuse empty launcher windows for document opens
Szowesgad Jun 9, 2026
6c9593b
[claude/interactive] fix(editor): disable re-entrant editor<->preview…
Szowesgad Jun 9, 2026
a81d2f1
[codex/vc-justdo] feat: add always-on-top transcription tafla
Szowesgad Jun 9, 2026
f5e29d8
feat(editor): add mocked autocomplete controller
Szowesgad Jun 9, 2026
7c0a531
[codex/vc-justdo] harden transcription tafla focus contract
Szowesgad Jun 9, 2026
3e1edc3
[codex/vc-justdo] feat: complete AI autocomplete UI
Szowesgad Jun 9, 2026
09ccd3c
[codex/vc-justdo] feat: complete transcription tafla daily loop
Szowesgad Jun 9, 2026
58435f2
[codex/vc-justdo] harden autocomplete cancellation
Szowesgad Jun 9, 2026
3b64ab5
fix(editor): harden AI autocomplete ghost text
Szowesgad Jun 9, 2026
33e2419
feat(tafla): dispatch kurier prompts to agents
Szowesgad Jun 9, 2026
e986484
feat(app): add DebugTrace and document window factory
Szowesgad Jun 11, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 0 additions & 11 deletions .claude/settings.local.json

This file was deleted.

15 changes: 15 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
name: CI

on:
push:
branches: [ "main" ]
pull_request:
branches: [ "main" ]

jobs:
test:
runs-on: macos-latest
steps:
- uses: actions/checkout@v4
- name: Run tests
run: make test
20 changes: 18 additions & 2 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,23 @@ dist/
/.claude/
/.vibecrafted/
/agents
*.md
!AGENTS.md
/.junie
*.txt
*.log
# Broad on purpose: internal docs (specs, plans, agent prompts) sync between
# machines over Tailscale + rsync, NOT through git/GitHub. Keep all markdown out
# except the named governance/root files below.
*.md
!README.md
!CODE_OF_CONDUCT.md
!CHANGELOG.md
!VERSION
!CONTRIBUTING.md
!SECURITY.md
!AGENTS.md
/tmp
/dist
/docs/superpowers
*.backup
/docs/workforce
/.claude
29 changes: 26 additions & 3 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -50,10 +50,33 @@ run: build ## Build + launch app (debug, no .app bundle)
run-release: release-local ## Build + launch signed .app (no notarize)
@open "$(APP_BUNDLE)"

.PHONY: install-app
install-app: release-local ## Build signed .app + swap the production bundle in /Applications
@printf "$(C_CYAN)[install]$(C_RESET) quitting running Pensieve — a live process + bundle swap = SIGKILL (code signature invalid)\n"
@osascript -e 'tell application id "io.vetcoders.pensieve" to quit' >/dev/null 2>&1 || true
@pkill -x Pensieve >/dev/null 2>&1 || true
@sleep 0.6
@printf "$(C_CYAN)[install]$(C_RESET) swapping /Applications/Pensieve.app\n"
@rm -rf "/Applications/Pensieve.app"
@ditto "$(APP_BUNDLE)" "/Applications/Pensieve.app"
@printf "$(C_CYAN)[install]$(C_RESET) relaunching from /Applications\n"
Comment on lines +59 to +62
@open "/Applications/Pensieve.app"
@printf "$(C_GREEN)[ ok ]$(C_RESET) installed $(APP_BUNDLE) → /Applications/Pensieve.app\n"

.PHONY: clean
clean: ## Remove .build/ + dist/
@printf "$(C_CYAN)[clean]$(C_RESET) removing .build + dist\n"
@rm -rf $(BUILD_DIR) $(DIST)
@# Rename-aside before delete: a live SourceKit/IDE indexer writing into
@# .build/index-build races a plain `rm -rf` (it repopulates a dir mid-delete,
@# so rmdir hits ENOTEMPTY). An atomic rename detaches the tree from the path
@# the indexer holds, so the delete can't lose that race. Best-effort: never
@# abort the gated release if a stray file lingers.
@for d in $(BUILD_DIR) $(DIST); do \
[ -e "$$d" ] || continue; \
trash="$$d.trash.$$$$"; \
mv "$$d" "$$trash" 2>/dev/null || trash="$$d"; \
rm -rf "$$trash" 2>/dev/null || rm -rf "$$trash" 2>/dev/null || true; \
done
@printf "$(C_GREEN)[ ok ]$(C_RESET) cleaned\n"

.PHONY: clean-deep
Expand Down Expand Up @@ -81,12 +104,12 @@ lint: ## Required format check; fails if swift-format is missing
printf "$(C_YELLOW)[missing]$(C_RESET) swift-format is required for lint/release gates (brew install swift-format)\n"; \
exit 1; \
fi
@cd $(PKG_DIR) && swift-format lint --recursive Sources/ Tests/
@cd $(PKG_DIR) && swift-format lint $$(find Sources Tests -name '*.swift' ! -path 'Sources/Pensieve/VistaBridge/qube_ffi.swift' -print)

.PHONY: format
format: ## Apply swift-format in-place when installed (best-effort helper)
@command -v swift-format >/dev/null 2>&1 \
&& cd $(PKG_DIR) && swift-format format --in-place --recursive Sources/ Tests/ \
&& cd $(PKG_DIR) && swift-format format --in-place $$(find Sources Tests -name '*.swift' ! -path 'Sources/Pensieve/VistaBridge/qube_ffi.swift' -print) \
|| printf "$(C_YELLOW)[skip]$(C_RESET) swift-format not installed (brew install swift-format)\n"

.PHONY: gates
Expand Down
19 changes: 19 additions & 0 deletions Pensieve/Package.swift
Original file line number Diff line number Diff line change
@@ -1,8 +1,12 @@
// swift-tools-version:5.9
// Pensieve — native macOS markdown editor (file-first, source-first)

import Foundation
import PackageDescription

let packageRoot = URL(fileURLWithPath: #filePath).deletingLastPathComponent().path
let qubeFFILibraryPath = "\(packageRoot)/Vendor/qube-ffi/debug"

let package = Package(
name: "Pensieve",
platforms: [
Expand All @@ -19,6 +23,7 @@ let package = Package(
.executableTarget(
name: "Pensieve",
dependencies: [
"qube_ffiFFI",
.product(name: "Markdown", package: "swift-markdown"),
.product(name: "GRDB", package: "GRDB.swift")
],
Expand All @@ -27,9 +32,23 @@ let package = Package(
.copy("Resources/markdown.css"),
.copy("Resources/gfm.css"),
.copy("Resources/mermaid.min.js"),
.copy("Resources/katex.min.js"),
.copy("Resources/katex.inline.min.css"),
.copy("Resources/sample.md")
],
linkerSettings: [
.unsafeFlags([
"-L", qubeFFILibraryPath,
"-lqube_ffi",
"-Xlinker", "-rpath",
"-Xlinker", qubeFFILibraryPath
])
]
Comment on lines +39 to 46
),
.systemLibrary(
name: "qube_ffiFFI",
path: "Sources/qube_ffiFFI"
),
.testTarget(
name: "PensieveTests",
dependencies: [
Expand Down
154 changes: 154 additions & 0 deletions Pensieve/Sources/Pensieve/App/AgentPromptDispatcher.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,154 @@
import Foundation

struct AgentDispatchMetadata: Equatable, Sendable {
let runID: String?
let reportPath: String?
let exitCode: Int32
let output: String

var statusLine: String {
if exitCode != 0 {
return "Dispatch failed (exit \(exitCode))"
}

switch (runID, reportPath) {
case (let runID?, let reportPath?):
return "Dispatch completed: \(runID) | \(reportPath)"
case (let runID?, nil):
return "Dispatch completed: \(runID)"
case (nil, let reportPath?):
return "Dispatch completed: \(reportPath)"
case (nil, nil):
return "Dispatch completed"
}
}

static func parse(output: String, exitCode: Int32) -> AgentDispatchMetadata {
AgentDispatchMetadata(
runID: firstMatch(
in: output,
patterns: [
#"(?m)^\s*run_id:\s*([^\s]+)\s*$"#,
#"(?m)^\s*Run ID:\s*([^\s]+)\s*$"#,
]),
reportPath: firstMatch(
in: output,
patterns: [
#"(?m)^\s*Report path:\s*(\S+)\s*$"#,
#"(?m)^\s*report_path:\s*(\S+)\s*$"#,
// Last-resort fallback: an absolute local path; the lookbehind keeps it from
// matching inside URLs ("https://host/reports/x.md") or protocol-relative refs.
#"(?<![/:])(/[^/\s][^\s]*/reports/[^\s]+\.md)"#,
]),
exitCode: exitCode,
output: output
)
}

private static func firstMatch(in text: String, patterns: [String]) -> String? {
for pattern in patterns {
guard let regex = try? NSRegularExpression(pattern: pattern) else { continue }
let range = NSRange(text.startIndex..<text.endIndex, in: text)
guard let match = regex.firstMatch(in: text, range: range), match.numberOfRanges > 1,
let matchRange = Range(match.range(at: 1), in: text)
else {
continue
}
return String(text[matchRange])
}
return nil
}
}

protocol AgentPromptLaunching: Sendable {
func dispatch(prompt: String, workingDirectoryURL: URL) throws -> AgentDispatchMetadata
}

enum AgentPromptLauncherError: LocalizedError {
case executableNotFound(searchedPaths: [String])

var errorDescription: String? {
switch self {
case .executableNotFound(let searchedPaths):
let searched = searchedPaths.joined(separator: ", ")
return
"vibecrafted executable not found (searched: \(searched)). "
+ "Set the PENSIEVE_VIBECRAFTED_PATH environment variable to the full path "
+ "of the vibecrafted script, or install vibecrafted under "
+ "~/.local/share/vibecrafted/tools/vibecrafted-current/scripts/vibecrafted."
}
}
}

final class VibecraftedAgentPromptLauncher: AgentPromptLaunching, @unchecked Sendable {
static let executablePathEnvironmentKey = "PENSIEVE_VIBECRAFTED_PATH"
static let defaultExecutableRelativePath =
".local/share/vibecrafted/tools/vibecrafted-current/scripts/vibecrafted"

static func resolveExecutablePath() throws -> String {
var candidates: [String] = []
if let override = ProcessInfo.processInfo.environment[executablePathEnvironmentKey],
!override.isEmpty
{
candidates.append(override)
}
candidates.append(
FileManager.default.homeDirectoryForCurrentUser
.appendingPathComponent(defaultExecutableRelativePath).path)

for candidate in candidates where FileManager.default.isExecutableFile(atPath: candidate) {
return candidate
}
throw AgentPromptLauncherError.executableNotFound(searchedPaths: candidates)
}

func dispatch(prompt: String, workingDirectoryURL: URL) throws -> AgentDispatchMetadata {
let executablePath = try Self.resolveExecutablePath()
let process = Process()
process.executableURL = URL(fileURLWithPath: executablePath)
process.arguments = ["implement", "codex", "--prompt", prompt]
process.currentDirectoryURL = workingDirectoryURL

let stdout = Pipe()
let stderr = Pipe()
let buffer = ProcessOutputBuffer()
process.standardOutput = stdout
process.standardError = stderr

stdout.fileHandleForReading.readabilityHandler = { handle in
buffer.append(handle.availableData)
}
stderr.fileHandleForReading.readabilityHandler = { handle in
buffer.append(handle.availableData)
}

try process.run()
process.waitUntilExit()

stdout.fileHandleForReading.readabilityHandler = nil
stderr.fileHandleForReading.readabilityHandler = nil
buffer.append(stdout.fileHandleForReading.availableData)
buffer.append(stderr.fileHandleForReading.availableData)

return AgentDispatchMetadata.parse(output: buffer.text(), exitCode: process.terminationStatus)
}
}

private final class ProcessOutputBuffer: @unchecked Sendable {
private let lock = NSLock()
private var data = Data()

func append(_ next: Data) {
guard !next.isEmpty else { return }
lock.lock()
data.append(next)
lock.unlock()
}

func text() -> String {
lock.lock()
let snapshot = data
lock.unlock()
return String(data: snapshot, encoding: .utf8) ?? ""
}
}
Loading
Loading