Freebuff Desktop — phantom empty "New thread" tabs are auto-created (home-tab flag never persists)
Product: Freebuff Desktop (Electron app)
Version: 0.0.62 (Windows x64)
Severity: medium (UX/data hygiene — pollutes the workspace DB with empty orphan threads; no data loss)
Reported: 2026-08-15
Summary
Freebuff Desktop silently creates empty "New thread" tabs on its own, repeatedly, throughout a session.
In one day the app created 7+ empty threads without any user action (17:11, 17:42, 17:44, 17:47, 18:51,
19:18:53, 19:19:41 local time). The threads are created with harness_id = NULL, zero messages, and stay in
the workspace DB (desktop-v2.db) as orphan rows. Users see tabs "opening by themselves".
Reproduction
No special steps needed — it happens on its own in any long session:
- Start Freebuff Desktop and open a workspace.
- Use the app normally (send prompts in any thread, switch tabs, let turns run).
- Periodically, an empty "New thread" tab appears without any user action — and a new thread row is
inserted into desktop-v2.db (threads table, harness_id = NULL, no messages).
Observed trigger in one session: sending a prompt (which got queued because the previous turn was still
running) was followed ~26 s later by a phantom thread; a second one appeared 48 s later; closing the home
tab re-created a new one immediately (verified live: close at 19:35:25, new thread created at 19:35:23).
Root cause (verified in the shipped bundle)
Three behaviors combine:
1. The home tab flag never survives persistence
resources/orchestrator/orchestrator.js:
zTabs = list(object({ id, projectPath, openerId, file })) — the schema has no home field, so
normalizeWorkspace() strips home on every read (zod drops unknown keys).
writeWorkspace(state) { this.write({ workspace: normalizeWorkspace(state) }) } — normalizeWorkspace
is applied before writing, so even though coerceTabs preserves home:true on the write path, the
value is removed again before it reaches state.json.
- Result:
~/.config/freebuff-desktop/state.json never contains a home flag. Confirmed live — the file
currently holds 4 tabs, none with home.
2. The UI auto-creates a home tab whenever none exists
resources/orchestrator/ui/assets/index-BRA_upWN.js (minified, function hJ):
function hJ(e){const t=L.useRef(!1);L.useEffect(()=>{
e||t.current||(t.current=!0,wy({pickProject:!1,home:!0}).finally(()=>{t.current=!1}))
},[e])}
hJ(t) is mounted in the main window component (uJ) with t = tabs.some(p => p.home). Whenever t is
falsy (at mount, or whenever the flag flips), it calls wy({pickProject:false, home:true}) → creates a
new empty thread and opens it as the "Home" tab. Because of bug #1, after any UI (re)initialization the
app always believes there is no home tab.
3. Sending a message from the home tab un-homes it, triggering another auto-create
In the composer (j9, submit handler Re):
o ? (edit) : Le ? U(e,ee,r,s) : (n && Z(e), O(e,ee,r,s)) // Z = promoteHome
n is the home prop of the composer. When the user sends a normal message from the home tab,
promoteHome(e) runs first — it removes the home flag from that tab:
promoteHome(n){const i=t().tabs.find(r=>r.id===n);
i!=null&&i.home&&(e(r=>({tabs:r.tabs.map(s=>s.id===n?{...s,home:void 0}:s)})),mi())}
→ tabs.some(p=>p.home) becomes false → hJ fires → a new empty home tab is created while the user
keeps working in the old thread. The old home tab is not closed in the UI, and its thread row stays in
the DB forever (status open) unless manually closed.
Also note openTab(home:true) replaces the previous home tab in the UI store (tabs: [{new home}, ...rest])
without closing the old thread — so each replacement leaves another orphan row.
Impact
- Multiple empty, unnamed threads accumulate in
desktop-v2.db per session (7+ in one day here).
- Users see unexplained tabs appearing; the "Home" tab silently rotates.
- Any automation that watches the DB for new threads gets spurious entries.
Suggested fix
- Persist
home: add home: zBoolean.optional().catch(undefined) (or equivalent) to the zTabs
schema in orchestrator.js, and stop re-normalizing on write (writeWorkspace should write what the
UI sent, or normalizeWorkspace must not drop the flag).
- Don't un-home on send:
promoteHome in the composer submit handler looks unintended — sending a
message from the home tab should not strip the home flag (and should certainly not spawn a replacement
tab). If the intent is "once used, the home tab becomes a normal tab", create the replacement only
once per session at boot, not on every message, and close the old one explicitly.
- Make
hJ non-recursive: gate the auto-create to initial mount only (e.g., guard with a module-level
"created once" flag), so closing/replacing a home tab never re-spawns one.
Environment
- Windows 11 x64, Freebuff Desktop 0.0.62 (install dir
%LOCALAPPDATA%\Programs\@codebufffreebuff-desktop)
- Workspace state:
~/.config/freebuff-desktop/state.json
- Thread DB:
<workspace>/.freebuff/desktop-v2.db
Freebuff Desktop — phantom empty "New thread" tabs are auto-created (home-tab flag never persists)
Product: Freebuff Desktop (Electron app)
Version: 0.0.62 (Windows x64)
Severity: medium (UX/data hygiene — pollutes the workspace DB with empty orphan threads; no data loss)
Reported: 2026-08-15
Summary
Freebuff Desktop silently creates empty "New thread" tabs on its own, repeatedly, throughout a session.
In one day the app created 7+ empty threads without any user action (17:11, 17:42, 17:44, 17:47, 18:51,
19:18:53, 19:19:41 local time). The threads are created with
harness_id = NULL, zero messages, and stay inthe workspace DB (
desktop-v2.db) as orphan rows. Users see tabs "opening by themselves".Reproduction
No special steps needed — it happens on its own in any long session:
inserted into
desktop-v2.db(threadstable,harness_id = NULL, no messages).Observed trigger in one session: sending a prompt (which got queued because the previous turn was still
running) was followed ~26 s later by a phantom thread; a second one appeared 48 s later; closing the home
tab re-created a new one immediately (verified live: close at 19:35:25, new thread created at 19:35:23).
Root cause (verified in the shipped bundle)
Three behaviors combine:
1. The
hometab flag never survives persistenceresources/orchestrator/orchestrator.js:zTabs = list(object({ id, projectPath, openerId, file }))— the schema has nohomefield, sonormalizeWorkspace()stripshomeon every read (zod drops unknown keys).writeWorkspace(state) { this.write({ workspace: normalizeWorkspace(state) }) }—normalizeWorkspaceis applied before writing, so even though
coerceTabspreserveshome:trueon the write path, thevalue is removed again before it reaches
state.json.~/.config/freebuff-desktop/state.jsonnever contains ahomeflag. Confirmed live — the filecurrently holds 4 tabs, none with
home.2. The UI auto-creates a home tab whenever none exists
resources/orchestrator/ui/assets/index-BRA_upWN.js(minified, functionhJ):hJ(t)is mounted in the main window component (uJ) witht = tabs.some(p => p.home). Whenevertisfalsy (at mount, or whenever the flag flips), it calls
wy({pickProject:false, home:true})→ creates anew empty thread and opens it as the "Home" tab. Because of bug #1, after any UI (re)initialization the
app always believes there is no home tab.
3. Sending a message from the home tab un-homes it, triggering another auto-create
In the composer (
j9, submit handlerRe):nis thehomeprop of the composer. When the user sends a normal message from the home tab,promoteHome(e)runs first — it removes thehomeflag from that tab:→
tabs.some(p=>p.home)becomes false →hJfires → a new empty home tab is created while the userkeeps working in the old thread. The old home tab is not closed in the UI, and its thread row stays in
the DB forever (status
open) unless manually closed.Also note
openTab(home:true)replaces the previous home tab in the UI store (tabs: [{new home}, ...rest])without closing the old thread — so each replacement leaves another orphan row.
Impact
desktop-v2.dbper session (7+ in one day here).Suggested fix
home: addhome: zBoolean.optional().catch(undefined)(or equivalent) to thezTabsschema in
orchestrator.js, and stop re-normalizing on write (writeWorkspaceshould write what theUI sent, or
normalizeWorkspacemust not drop the flag).promoteHomein the composer submit handler looks unintended — sending amessage from the home tab should not strip the home flag (and should certainly not spawn a replacement
tab). If the intent is "once used, the home tab becomes a normal tab", create the replacement only
once per session at boot, not on every message, and close the old one explicitly.
hJnon-recursive: gate the auto-create to initial mount only (e.g., guard with a module-level"created once" flag), so closing/replacing a home tab never re-spawns one.
Environment
%LOCALAPPDATA%\Programs\@codebufffreebuff-desktop)~/.config/freebuff-desktop/state.json<workspace>/.freebuff/desktop-v2.db