Skip to content

Pr/workspace bugs#105

Closed
Midway65 wants to merge 69 commits into
ProfSynapse:mainfrom
Midway65:pr/workspace-bugs
Closed

Pr/workspace bugs#105
Midway65 wants to merge 69 commits into
ProfSynapse:mainfrom
Midway65:pr/workspace-bugs

Conversation

@Midway65
Copy link
Copy Markdown

@Midway65 Midway65 commented Apr 7, 2026

fix(workspace): workspace selection and restore reliability

Three targeted bug fixes in the workspace/chat-settings pipeline. No architectural changes — each fix is a correctness correction to existing behaviour.

Depends on: pr2-workspace-two-tier-prompt must be merged first (Bug 3 references pendingFullWorkspaceLoad introduced there).

Commits: 996c578f, 25ec9a62, 699bd008, 986eb384
Files changed: 2

  • src/ui/chat/components/ChatSettingsModal.ts
  • src/ui/chat/ChatView.ts

Bug 1 — Workspace selection silently skipped for 14 of 21 workspaces

ChatSettingsModal.handleSave() had a guard that called setWorkspaceContext() only when workspace.context was non-null. Workspaces without a contextJson column value — all user-created workspaces — hit this guard and were silently dropped. The workspace ID was never bound, the system prompt header was never populated, the session was never set. The UI appeared to select the workspace but state was not applied.

Fix: Remove the guard. Remove the redundant caller-side getWorkspace() fetch. setWorkspaceContext() now takes only workspaceId and derives context internally.

// Before
const workspace = await this.workspaceService.getWorkspace(settings.workspaceId);
if (workspace?.context) {
    await this.modelAgentManager.setWorkspaceContext(settings.workspaceId, workspace.context);
}

// After
await this.modelAgentManager.setWorkspaceContext(settings.workspaceId);

setWorkspaceContext signature changed from (workspaceId: string, context: WorkspaceContext) to (workspaceId: string). The caller no longer needs to fetch the workspace first; context is derived from getWorkspaceBasic() inside the method.


Bug 2 — Workspace restore wiped selectedWorkspaceId at startup

restoreWorkspace() called loadWorkspace() — a full MCP agent execution — to reconstruct workspace state on startup and on every conversation switch. If the agent was not ready (common at startup), it returned null and the catch/null branch cleared selectedWorkspaceId. The workspace ID was saved in conversation metadata but lost from in-memory state. On modal reopen the dropdown showed empty.

Fix: restoreWorkspace() now uses getWorkspaceBasic() — a direct SQLite query with no agent dependency, added to WorkspaceIntegrationService. selectedWorkspaceId is set tentatively before the await and cleared only if the workspace is genuinely absent from the DB, not on transient errors.

// New method in WorkspaceIntegrationService — cheap DB lookup, safe at any startup phase
async getWorkspaceBasic(workspaceId: string): Promise<{
  id: string; name: string; description?: string;
  rootFolder?: string; context?: Record<string, unknown>;
} | null>

Bug 3 — Full-load flag consumed during initialization, not on first user message

performFullInitialization() in ChatView.ts called getMessageOptions() at line 298 to read the current provider for a WebLLM pre-load hint. This fired the G-W3 block as a side effect: it loaded full workspace data, set pendingFullWorkspaceLoad = false, and cleared loadedWorkspaceData — all before the user sent any message. The user's first actual message then found the flag already consumed and received a slim header only. The full-load-on-first-message feature worked correctly for workspace selections (where setWorkspaceContext() fires after initialization) but was silently broken for workspace restores.

Fix: Replace the getMessageOptions() call with getSelectedModel()?.providerId — sync, no side effects, same data, already exists on ModelAgentManager.

// Before — triggered G-W3 block as a side effect
const currentProvider = (await this.modelAgentManager.getMessageOptions()).provider;

// After — reads provider directly, no side effects
const currentProvider = this.modelAgentManager.getSelectedModel()?.providerId;

Testing

All three bugs verified in a live Obsidian vault (2026-04-06).

Bug 1: ✅ PASS

  1. Created a new workspace (no contextJson).
  2. Opened chat settings, selected the workspace and a prompt, saved.
  3. Model responded correctly with workspace context applied. Reopened settings — all selections retained.

Bug 2: ✅ PASS

  1. Selected a workspace in a conversation and saved.
  2. Restarted Obsidian.
  3. Reopened the conversation — chat settings dropdown showed the correct workspace immediately.

Bug 3: ✅ PASS (verified via first-message token count and model behaviour — see pr2-workspace-two-tier-prompt.md testing section, which covers the full-load path this fix enables.)

Midway65 and others added 30 commits April 3, 2026 20:49
- 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>
- 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>
Midway65 and others added 23 commits April 5, 2026 15:30
…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>
…s in chat settings

setWorkspaceContext() now accepts WorkspaceContext | null. ChatSettingsModal
no longer skips the call when workspace.context is null, so all 21 workspaces
are selectable from chat settings regardless of whether they have contextJson.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…ntext

Context is now derived from the getWorkspaceBasic() call already inside the
method — the caller no longer needs to fetch the workspace first. ChatSettingsModal
passes only the workspaceId; no upstream name change.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
ChatView.performFullInitialization() was calling getMessageOptions() at
line 298 to read the provider for a pre-load hint. This fired the G-W3
block prematurely — loading full workspace data, clearing loadedWorkspaceData,
and setting pendingFullWorkspaceLoad=false — before the user sent any message.
First message after workspace restore therefore always received slim header only.

Replace with getSelectedModel()?.providerId — sync, no side effects, already
exists on ModelAgentManager. G-W3 now fires correctly on actual first send.

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 added a commit that referenced this pull request Apr 7, 2026
…al upstream bug

The workspace?.context guard was a real bug on main (Sep 2025), not
introduced by Midway's refactor. We fixed it in PR #107. Updated the
draft to credit the find and reference our fix.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@ProfSynapse
Copy link
Copy Markdown
Owner

Hi @Midway65, thanks for the continued work here — you keep finding real things worth looking at.

What we took

We pulled out the dead per-turn fetch removal and landed it on fix/remove-dead-per-turn-fetches. You were right that vaultStructure, availableWorkspaces, availablePrompts, and toolAgents were being fetched every message send and never consumed by the builder — I'd removed the consumption side a while back but never cleaned up the fetch side. That's now gone (-110 lines).

We also fixed the workspace?.context guard you identified — that was a real upstream bug dating back to Sep 2025 where most user-created workspaces were silently skipped during selection. Landed in #107.

Why we can't merge this PR as-is

1. It carries the full PR #97 payload. This PR is 40 files / 66 commits / +2,250/-1,896 but the workspace feature only touches ~5 files across 8 commits. The rest is the PR #97 content we already went through — schema migrations v12-v19, the action bar feature, the JSONL orphan pruning, and ~1,500 lines of CRLF whitespace conversion. All of that is still bundled in here, which makes it impossible to review or merge cleanly.

2. The two-tier system prompt doesn't match our architecture. The system prompt gets rebuilt before each message send and is always sent at position zero of the completion. The full workspace blob goes in there so the LLM is oriented to the workspace on every turn — it needs the folder tree, sessions, and context to function properly.

The "slim header on turn 2+" approach (G-W2/G-W3) saves tokens, but the LLM loses the folder tree and workspace context after the first message. Telling it to "call memoryManager.loadWorkspace if you need details" adds a round-trip and defeats the purpose — the LLM should just know what workspace it's in without having to ask. We need the full blob every time.

This also means the "cheap restore" (G-W1) can't be separated out — on main, the restoreWorkspace call via loadWorkspace() is what populates loadedWorkspaceData, which is what the system prompt builder serializes. Replacing that with a basic DB lookup would remove the folder tree from the system prompt.

3. Two of the 3 bug fixes address bugs introduced by the refactor, not bugs on main. The G-W3 flag race condition only exists because the G-W3 flag was introduced in this PR. The redundant context parameter is only redundant after the setWorkspaceContext refactor. These are valid fixes within your branch, but they don't address issues in the upstream codebase.

The workspace?.context guard that blocked 14/21 workspaces was a good catch — that one was a real bug on main (dating back to Sep 2025). We've fixed it separately in #107.

Specific items not taken and why

Item Why not
Schema migrations v12-v19 Fork-specific cleanup for your prior Nomic/768-dim branch. Burns 8 schema version numbers that don't help upstream users.
Action bar (Insert/Append/Create File) Previously discussed in #97 — adds UI complexity we don't want right now.
JSONL orphan pruning at startup Deletes JSONL files not found in SQLite, but JSONL is our source of truth. If SQLite is corrupt/incomplete, this permanently destroys conversation data.
Two-tier prompt (G-W2/G-W3) LLM needs full workspace context every turn, not just on the first message.
Cheap restore (G-W1) Breaks the system prompt — loadWorkspace() is what populates the data serialized into the prompt.
G-W3 flag + redundant context param fixes Fix bugs introduced by this PR's refactor, not bugs on main.
workspace?.context guard fix Real upstream bug — good catch. Fixed separately in #107.
ProviderHttpClient require() switch Already fixed on main via the mobile compat work (desktopRequire()).
CRLF whitespace changes ~1,500 lines of noise across 10 files that inflate the diff.

Going forward

One thing that would really help — please pull main so your fork is up to date before submitting PRs. Right now your branch has diverged significantly and every PR carries months of accumulated fork-specific changes mixed in with the new work. That makes it very hard to isolate what's actually new.

For the kind of issues you're finding, opening issues might work better than PRs. You clearly have good instincts for spotting inefficiencies — the dead per-turn fetches were a real find. If you file those as issues with the detail you put in your commit messages, we can implement fixes that fit cleanly on main without the merge headaches.

Thanks again for the time you put into this.

@ProfSynapse ProfSynapse closed this Apr 7, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants