Pr/workspace two tier prompt#104
Closed
Midway65 wants to merge 66 commits into
Closed
Conversation
- minHeight 48→72px (desktop) and 64px (mobile) to match CSS values - maxHeight 120→200px (desktop) and 160px (mobile) to match CSS values - Replace class-toggle height measurement with style.removeProperty() for reliable scrollHeight reads on contenteditable divs Fixes BUG-01: input box shrinking unexpectedly while typing. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Instead of calling content.empty() and re-rendering all steps on every refresh(), reconcile existing DOM elements with current state: - Existing steps are updated in place (status class, text, meta, result) - New steps are appended only when first seen - Reasoning items follow the same pattern Eliminates layout thrashing during active tool execution. Fixes BUG-02: accordion formatting shifting on every update. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The variable was computed but never used. Retry functionality is fully handled by MessageAlternativeService — this was leftover from a planned but unimplemented streaming differentiation path. Fixes BUG-04. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Removed the experimental/beta warning banner that appeared on every chat open. The chat window is stable enough for daily use and the warning was more distracting than informative. Removed: createWarningBanner() method, its call site, all associated CSS classes (.chat-experimental-warning and children, .chat-warning-banner-fadeout). Settings-tab warning styles untouched. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Adds a MessageActionBar pill below every completed AI message that has text content. Moves the copy action out of the bubble header and into the pill alongside Insert at cursor, Append to active note, and Create new file. CreateFileModal handles naming, folder selection (default 00-inbox), and optional open-after-save. Pill fades to 35% opacity at rest and full opacity on hover. - src/ui/chat/components/CreateFileModal.ts (new) - src/ui/chat/components/MessageActionBar.ts (new) - src/ui/chat/components/MessageBubble.ts — appendActionBar, cleanupActionBar, remove header copy button for assistant messages - src/ui/chat/components/factories/ToolBubbleFactory.ts — remove copy button from createTextBubble, drop onCopy/showCopyFeedback params - styles.css — message-action-bar and message-action-bar-btn styles Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
onMessageAlternativeChanged and component were only used by the copy button logic that was removed in the previous commit. Stale header comment in MessageBubble also updated. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Satisfies @typescript-eslint/no-unused-vars. Parameter was never used in the function body even before this feature — only surfaced now. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The .message-actions-external pill had opacity:0 but no pointer-events:none, causing it to silently intercept all mouse clicks over message content. Text selection was broken because the invisible pill sat above the content in the stacking context (z-index 20). Added pointer-events:none to the hidden state and pointer-events:auto to all visible states (hover, mobile, media queries). Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The separate bottom pill (message-action-bar) was wrong placement and left the message-actions-external div empty. An empty sticky/z-20 element with no pointer-events:none silently blocked all mouse clicks on message content, breaking text selection. Fix: - MessageActionBar.renderInto(el) populates the existing upper-right .message-actions-external pill instead of creating a new element - MessageActionBar.removeFromContainer() cleans up on rebuild/destroy - appendActionBar() queries .message-actions-external within the container rather than appending a new child after the bubble - Removed .message-action-bar CSS block (no longer needed) - pointer-events:none already in place for the hidden pill state All 4 buttons (Copy, Insert, Append, Create File) now appear in the familiar upper-right corner pill alongside the branch navigator. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…city Two fixes: 1. .message-content now explicitly sets user-select:text and cursor:text. Obsidian's Electron shell applies user-select:none globally; without an explicit override the content was not text-selectable. Child elements (links, buttons) retain their own cursor/pointer-events. 2. .message-actions-external resting opacity changed from 0 to 0.25 so the pill is always visible as a subtle affordance, becoming fully opaque on hover. Removed stale transform from hover rule. Mobile resting opacity set to 0.75 (was 0.85). Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Pill was sitting inside the bubble with sticky+float positioning, leaving it detached from the header and occasionally blocking layout. Moved it into .message-header for both the standard path (MessageBubble) and the group path (ToolBubbleFactory). The header's justify-content:space-between naturally places the bot icon left and the pill right with no extra CSS. Also reduced pill transition from 0.2s to 0.1s to reduce repaint churn during text selection drag operations. Removed stale z-index and mobile top/right overrides that no longer apply. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Clicking Insert or Append shifted focus from the active note to the chat panel button, causing getActiveViewOfType(MarkdownView) to return null and silently doing nothing. Added mousedown:preventDefault on both buttons so editor focus and cursor position are preserved through the click. Also added user-visible Notice for the no-active-note case. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
getActiveViewOfType(MarkdownView) returns null whenever the chat panel
is the active workspace view, even if a note is open alongside it.
Added getMarkdownView() helper that falls back to getLeavesOfType
('markdown') so Insert and Append work regardless of which panel last
received focus. Also added editor.focus() before replaceSelection so
the cursor is visible after insertion.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Default folder was '00-inbox' — corrected to '00-Inbox'. Toggle value was read from a class field updated via onChange, but close() runs before the check and could leave state stale. Fix: store the ToggleComponent ref and call getValue() at create time instead. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Prevents Obsidian from auto-updating this fork if/when the upstream ProfSynapse/nexus repo is accepted into the community plugins registry. Obsidian matches plugins by id — "nucleus" will never match "nexus". Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Upstream changes: - VaultIngestionManager: moved drag-and-drop ingest UI out of ChatView into dedicated VaultIngestionManager (src/core/ingest/VaultIngestionManager.ts) - PdfJsLoader: new lazy-loader for pdfjs-dist to improve startup performance - ChatView/ChatLayoutBuilder: removed inline ingest UI (now in VaultIngestionManager) - DefaultsTab: new ingestion settings fields - PdfPageRenderer/PdfTextExtractor: bug fixes and improvements - types.ts / PluginTypes.ts: new enableIngestion flag support Conflict resolutions: - manifest.json: kept id=nucleus/name=Nucleus, took upstream version=5.6.2 - ChatView.ts: took upstream (removed inline ingest imports and methods); our action buttons and beta-banner-removal changes are in separate sections and were auto-merged cleanly by git - ChatLayoutBuilder.ts: auto-resolved cleanly by git Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Reverts the nucleus rename — it caused the chat plugin to stop functioning. Plugin identity restored to id=nexus/name=Nexus. Version remains 5.6.2 (from the upstream merge). Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
DOCX, PPTX, XLSX ingestion support. New services: DocxExtractionService, PptxExtractionService, SpreadsheetExtractionService. VaultIngestionManager updated for multi-output paths and xlsx null guard. Version bump 5.6.2→5.6.3. Conflicts resolved: CLAUDE.md, manifest.json, package.json (version bumps — took upstream), connectorContent.ts (kept our newer timestamp), ChatView.ts ×4 (whitespace only — took upstream), VaultIngestionManager.ts ×3 (new logic — took upstream). Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
any→unknown type migration, ESLint v9 + obsidianmd linter. Resolved 6 conflicts: kept our actionBar additions, beta banner deletion, copy button removal; took upstream type safety changes and two new type imports. Fixed 2 new lint errors in our files: no-misused-promises in MessageActionBar, no-unnecessary-type-assertion and sentence-case in ProgressiveToolAccordion.
…384-dim model note_embeddings and block_embeddings vec0 tables were created with float[768] from an older embedding model. Current model (TaylorAI/bge-micro-v2) produces 384-dim vectors. Drops and recreates both tables, clears associated metadata. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
vec0 virtual table DROP/CREATE cannot run via prepare().step() in the DatabaseAdapter. Migration v12 is now a version marker only; the actual fix runs in SQLiteCacheManager.fixVec0TableDimensions() using the raw WASM db.exec(), matching the same path used by clearAllData(). Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…config (v17) - CURRENT_SCHEMA_VERSION bumped 12 → 17 - Stubs v13-v16: acknowledge prior local-fixes fork era (Nomic pipeline, semantic panel, block embeddings) so version comparison stays accurate - Migration v17: DROP TABLE IF EXISTS embedding_config — orphaned table from old Nomic era; our fork never reads or writes it Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…dimension column The old local-fixes fork added a `dimension INTEGER NOT NULL` column to embedding_metadata between its v13-v16 migrations. That fork's strip commit would have removed it (at its v12/v13), but the live DB was already at v16 so the strip never ran. Our NoteEmbeddingService inserts without the dimension column, causing NOT NULL constraint failures on every note embedding attempt. Fix: drop and recreate embedding_metadata with the clean schema (no dimension column). Cache data is expendable — notes will be re-indexed. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…k_embedding_metadata tables
- Merged two body.is-mobile .message-actions-external blocks into one; updated stale comment (pill is in header, not below message) - Added transform 0.1s ease to .message-action-btn transition so the copy-success scale(1.1) animates instead of snapping Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
No file in the codebase imports or uses ContentProcessor. Confirmed via full-repo grep across all .ts and .js files. Build and lint pass with zero errors after removal. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Remove sonar-reasoning (deprecated Dec 15 2025) from PERPLEXITY_MODELS - Remove r1-1776 (removed Aug 1 2025) from PERPLEXITY_MODELS - Remove PERPLEXITY_OFFLINE_MODELS export (no offline models remain) - Simplify PERPLEXITY_SEARCH_MODELS to full model list (all models have search) - Add 'minimal' to PerplexityOptions.reasoningEffort type - Add 'sec' to PerplexityOptions.searchMode type - Mirror new literals in PerplexityRequestBody.extra types Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Remove extra wrapper; all search params now top-level per API spec - Add full search param set: search_recency_filter, search_domain_filter, search_after/before_date_filter, last_updated_after/before_filter, enable_search_classifier, disable_search, return_related_questions, return_images, search_language_filter, language_preference - Add web_search_options.search_type - Gate reasoning_effort and stream_mode to sonar-reasoning-pro only - Remove presence_penalty and frequency_penalty (not in Perplexity API spec) - Remove convertTools(), PerplexityToolDefinition, and tools field (Perplexity has no function calling) - Add stripUndefined() to keep serialised body clean - Extend PerplexityOptions with all new params Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
… complete
Adds two diagnostic console.warn entries:
- [BaseAdapter] logs finish_reason when detected ('stop', 'length', 'tool_calls')
- [StreamingOrchestrator] logs total char count when stream completes
These distinguish between a clean stop vs max_tokens truncation ('length')
and show how much content was actually received before completion.
Temporary diagnostic — to be removed once root cause is confirmed.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
onCopyMessage was reading message.content directly, which always holds the most-recently-streamed response (the retry). Branches hold the older alternatives. This caused copying page 1 to return page 2 content. Fix: read via branchManager.getActiveMessageContent so copy respects the currently displayed page. Also: expand StreamingOrchestrator completion log to include last 100 chars of content so we can see if responses are truncating mid-sentence. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Reverts all Perplexity-specific changes introduced in commits 852e895 through c7eb50b. Full-revert decision: API truncation is a hard architectural limit of Perplexity's sonar models (confirmed OQ-01). Files restored to 17a5eb8 state (git checkout): - PerplexityModels.ts — restores deprecated model IDs - PerplexityAdapter.ts — restores original request/response handling - MessageBubble.ts — removes citations panel + stripThinkingBlocks Surgical edits (Perplexity additions removed, non-Perplexity kept): - ToolContinuationService.ts — remove metadata? from StreamYield - StreamingOrchestrator.ts — remove finalMetadata tracking + tail log - StreamingResponseService.ts — remove metadata? from StreamingChunk/LLMChunkLike - ChatService.ts — remove metadata? from ChatStreamingChunk - MessageStreamHandler.ts — remove metadata? from StreamResult - styles.css — remove Perplexity citations CSS block - ProviderHttpClient.ts — remove diagnostic console.warn only (resRef fix kept) - BaseAdapter.ts — remove finish_reason + silent-close diagnostic warns Preserved (non-Perplexity fixes that landed during Work Stream C): - 1bb4fa8 — require() fix for node:https in Electron renderer - 5475d66 + ff2b6c5 — branch-nav double-render fix - 8059edb — JSONL bloat fix + large-file crash fix - c7eb50b (partial) — branch-aware copy fix in MessageDisplay.ts Lint: 0 errors. Build: PASS. Deployed. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…es on startup - ConversationRepository.delete(): add jsonlWriter.deleteFile() after the SQLite delete. Previously only the SQLite row was removed, leaving conv_*.jsonl files behind for every deleted conversation (O(n) orphan accumulation). - HybridStorageAdapter.pruneOrphanedConversationFiles(): new method that runs BEFORE sync/rebuild on every startup (when syncState exists). Lists all conversations/*.jsonl files, checks each ID against SQLite, and deletes any file with no matching record. Running before rebuild is critical — fullRebuild blindly replays all JSONL events and would resurrect deleted conversations if orphaned files were present. Skipped on first-ever startup (!syncState) to avoid deleting everything when SQLite is empty. Permanent safety net for any future delete failures. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
When the last conversation was deleted and the welcome state was shown, the header title kept displaying the deleted conversation's name. Added title reset in both handleConversationsChanged() and loadInitialData() wherever conversations.length === 0 triggers the welcome state. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- cleanup() now calls clearPendingDeleteConversation() so pendingDeleteTimer is cleared if the panel closes during the 5s confirmation window (E01-1) - showRenameInput() replaces two document.createElement calls with createEl per CLAUDE.md Obsidian API rules (E01-2) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Guard update() with a JSON equality check before calling render(). Prevents unbounded registerDomEvent accumulation in component._events on hot iteration paths (E01-3). Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
startLoadingAnimation() now clears any existing interval before setting a new one. Prevents the first interval leaking when called twice during a streaming message (E02-1). Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Delete never-called private escapeHtml() from MessageDisplay.ts (E02-2) - Delete never-called private escapeHtml() from StreamingController.ts (E05-2) - Remove void keyword from syncWorkspacePrompt() call in ChatSettingsRenderer.ts — method is synchronous (E06-3) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Replace className = '...' with removeAttribute('class') + addClass()
per Obsidian component conventions (E02-3).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
listConversations({ limit: 50 }) silently truncated users with more than
50 conversations. Raised to 500 to cover realistic usage (E04-1).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
handleSave() now writes defaultImageModel back to plugin settings and calls saveSettings() after updating all other fields. Previously the image model section in the per-chat modal was silently discarded (E06-1). Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Return empty sentinel { provider: '', model: '' } instead of openai/gpt-4o
when no configured default exists. Callers already guard on truthiness
so a non-OpenAI-only setup no longer silently selects an unavailable model
(E06-2).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- LLMProviderModalConfig.onSave now returns void | Promise<void> - autoSave() debounce callback awaits onSave and only shows "Saved" after resolution; shows "Save failed" on rejection (E07-2) - ProvidersTab onSave callback changed from void wrapper to async arrow function so the promise is properly returned - OAuth immediate-save path uses void (fire-and-forget, correct pattern) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
E08-1: Consolidate duplicate .chat-settings-modal into one rule
(padding/min-width/max-width). Remove stale h2 rule.
E08-2: Rename .chat-settings-button-container -> .chat-settings-buttons
to match ChatSettingsModal.ts:51 (flex/gap now applies to
Cancel/Save buttons).
E08-3: Delete orphaned .chat-settings-section rule.
E08-4: Delete pre-refactor orphan block: .workspace-info-section,
first .workspace-context-summary (+h4), .context-item (+strong),
.workspace-info-section p (all zero TS usage).
E08-5: Delete four orphaned context-notes-* / add-context-note-container
rules (replaced by csr-notes-*).
E08-6: Add missing .csr-temp-value rule (used in ChatSettingsRenderer.ts
but had no CSS definition; temperature span was unstyled).
E08-7: Merge duplicate .llm-provider-model-row into single rule
(absorb gap/margin-bottom from first into second).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Both adapter and legacy paths in getWorkspaceByNameOrId called ws.name.toLowerCase() without null-guarding. Workspaces with a null/ undefined name (which exist in the DB — getAllWorkspaces already filters them out) caused a persistent TypeError when loadWorkspace fell through from the ID lookup to the name-search fallback. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Three additional unguarded .name.toLowerCase()/.localeCompare() calls on workspace/state records that can have null names in the DB: - WorkspaceService.getWorkspaces() sort-by-name: a.name.localeCompare(b.name) - ToolBatchExecutionService.validateWorkspaceId: workspace.name.toLowerCase() - MemorySearchProcessor.searchStates/searchWorkspaces: state/workspace.name.toLowerCase() All guarded with optional chaining (?.) or nullish coalescing (?? ''). Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
G-W1: restoreWorkspace() now calls getWorkspaceBasic() (cheap DB lookup) instead of loadWorkspace() (full MCP agent). selectedWorkspaceId is set tentatively before the await so the modal never shows empty on transient errors. selectedWorkspaceId is only cleared when the workspace is genuinely absent from the DB. G-W4: Remove 4 dead per-turn fetches from buildSystemPromptWithWorkspace(): getVaultStructure(), listAvailableWorkspaces(), getAvailablePromptSummaries(), getToolAgentInfo() — confirmed not read by SystemPromptBuilder.build(). Also removes the now-dead private methods and associated interfaces/imports. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…in system prompt Instead of serialising the entire loadWorkspace agent result (~2k-10k tokens) into the system prompt on every turn, inject a compact <active_workspace> header (~100 tokens) built from cheap DB data already fetched by G-W1. - ModelAgentManager: new selectedWorkspaceSlimData field; populated by getWorkspaceBasic() in both setWorkspaceContext() and restoreWorkspace(); cleared in clearWorkspaceContext() and initializeDefaultModel(); passed to build() - SystemPromptBuilder: new selectedWorkspaceSlimData option; buildSelectedWorkspaceSection() rewired to emit slim header by default; full JSON blob path preserved for first-message send (G-W3), triggered when loadedWorkspaceData is non-null Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…thereafter On the first message after a workspace is selected (via settings save or conversation restore), getMessageOptions() triggers a single loadWorkspace() agent call and injects the full rich blob into that turn's system prompt. loadedWorkspaceData is cleared immediately after the prompt is built, so all subsequent turns use the slim header. pendingFullWorkspaceLoad flag lifecycle: - Set true: setWorkspaceContext() (user saves modal) and restoreWorkspace() (conversation switch) - Set false: initializeDefaultModel() (fresh start / conversation switch), clearWorkspaceContext(), and getMessageOptions() at the moment of first-message load Agent call on first send is gracefully degraded — failure logs and falls back to slim header. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…bagents SubagentController.getLoadedWorkspaceData() is called after the message send returns (during LLM tool-call processing), so clearing loadedWorkspaceData immediately after buildSystemPromptWithWorkspace() left subagents with null. Fix: move the clear to the START of getMessageOptions() instead of the end. Data is cleared before loading new data for the current turn, so it doesn't bleed into subsequent turns' system prompts, but remains available throughout the current message-processing cycle. Also fix gap: the "workspaceId explicitly null" restore path (lines 257-261) did not clear selectedWorkspaceSlimData or pendingFullWorkspaceLoad. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1. setWorkspaceContext — pendingFullWorkspaceLoad now set only when getWorkspaceBasic returns a workspace, not unconditionally before the fetch. Prevents a wasted loadWorkspace call on first message when the workspace ID is not found in DB. 2. buildSystemPromptWithWorkspace — stop passing workspaceContext to SystemPromptBuilder.build(); the field is not consumed by any section builder after the slim-header refactor. Field kept in SystemPromptOptions for upstream interface compatibility. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
ProfSynapse
pushed a commit
that referenced
this pull request
Apr 7, 2026
Both PRs carry the full PR #97 payload (previously partially rejected) with workspace optimization commits stacked on top. The new workspace work (two-tier prompt, cheap restore, dead fetch removal) and 3 bug fixes are all sound, but neither PR can merge as-is due to the PR #97 baggage (schema migrations v12-v19, action bar, JSONL pruning, whitespace noise). Recommend extracting the workspace commits into a clean PR rebased on main. https://claude.ai/code/session_01XbMN35zn6yxTpYWbNzh57h
ProfSynapse
pushed a commit
that referenced
this pull request
Apr 7, 2026
4 tasks
Owner
|
Hi @Midway65, thanks again for the continued effort here — you clearly have a solid understanding of the codebase and keep finding real things worth looking at. I'm going to close this one in favor of #105 since it's a strict superset (same base + 3 additional bug fix commits). No point reviewing both separately. See my comment on #105 for the full breakdown. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
feat(workspace): two-tier system prompt — slim header every turn, full load on first message
Replaces the per-turn full workspace JSON blob with a two-tier system: a ~100-token slim header on every turn, and a single full workspace load injected on the first message after a workspace is selected or restored. Also removes four dead per-turn async fetches.
Submit this PR before
pr1-workspace-bugs— Bug 3 in that PR referencespendingFullWorkspaceLoadintroduced here.Commits:
2f523359,f287fdc3,3b9ee3ea,9b5be688,3c150070Files changed: 3
src/ui/chat/services/ModelAgentManager.tssrc/ui/chat/services/SystemPromptBuilder.tssrc/ui/chat/services/WorkspaceIntegrationService.tsProblem
setWorkspaceContext()calledloadWorkspace()(full MCP agent execution) at workspace-select time and stored the result inloadedWorkspaceData.SystemPromptBuilder.buildSelectedWorkspaceSection()then serialised the entire object — sessions, file tree, tasks, workflows, key files, preferences, recent activity — as a JSON blob inside<selected_workspace>on every message turn. Cost: 2,000–10,000+ tokens per turn, compounding with conversation history.Four additional async calls ran inside
buildSystemPromptWithWorkspace()on every send and were passed toSystemPromptBuilder.build(), where none were consumed:getVaultStructure()— reads vault root from disklistAvailableWorkspaces()— DB querygetAvailablePromptSummaries()— DB querygetToolAgentInfo()— agent registry lookupAll four were fetched, allocated, and immediately discarded on every turn.
Fix — two-tier workspace section
Slim header (every turn): A ~100-token
<active_workspace>block containing onlyid,name,description,purpose, androotFolder, plus a hint to callmemoryManager.loadWorkspacefor details:Full blob (first message only): On the first message after a workspace is selected or restored, the full
loadWorkspaceresult is injected once into that turn's system prompt via apendingFullWorkspaceLoadflag. Subsequent turns revert to the slim header. The full data remains available toSubagentController.getLoadedWorkspaceData()during tool-call processing for the current turn; it is cleared at the start of the nextgetMessageOptions()call (not at the end of the current one — this preserves subagent access during the current message cycle).The four dead per-turn fetches are removed entirely.
New fields on
ModelAgentManagerSystemPromptOptionsinterfaceWorkspaceIntegrationService— new methodUsed by
restoreWorkspace()(cheap identity restore) andsetWorkspaceContext()(slim data population).State management
All clear paths —
clearWorkspaceContext(),initializeDefaultModel(), and the null-restore branch inrestoreFromConversationMetadata()— now consistently clear all five workspace-related fields:selectedWorkspaceIdworkspaceContextloadedWorkspaceDatapendingFullWorkspaceLoadselectedWorkspaceSlimDataTwo code-review corrections applied in a follow-up commit:
pendingFullWorkspaceLoadis now set only inside theif (slim)branch ofsetWorkspaceContext()— not before the DB fetch. Prevents a wastedloadWorkspacecall if the workspace does not exist.workspaceContext: this.workspaceContextpass removed from thebuild()call inbuildSystemPromptWithWorkspace(). The field is kept inSystemPromptOptionsfor upstream interface compatibility.WorkflowRunService— confirmed safeWorkflowRunServiceis a second caller ofSystemPromptBuilder.build(). It passestoolAgents,vaultStructure,availableWorkspaces— all still present inSystemPromptOptions. No breakage.Testing
Verified in a live Obsidian vault (2026-04-06).
Two-tier prompt:
02-Projects/Nexus) and reading key state files — confirming full<selected_workspace>blob fired on the first message.