diff --git a/src/__tests__/dashboard-workspaces.test.ts b/src/__tests__/dashboard-workspaces.test.ts
index a09ad8865..30bfc2095 100644
--- a/src/__tests__/dashboard-workspaces.test.ts
+++ b/src/__tests__/dashboard-workspaces.test.ts
@@ -134,4 +134,15 @@ describe('dashboardWorkspaces discovery (#809)', () => {
expect(p, JSON.stringify(workspaces)).toBeDefined();
expect(p && workspaceEvents(events, p, workspaces).map(e => e.sessionId)).toEqual(['s1']);
});
+
+ it('lists the unassigned bucket for a session that recorded no cwd (Copilot, #1121)', async () => {
+ home = fs.realpathSync.native(fs.mkdtempSync(path.join(os.tmpdir(), 'teamai-ws-discovery-')));
+ vi.stubEnv('HOME', home);
+ const copilot: DashboardEvent = { type: 'session_start', timestamp: '2026-09-17T00:00:00Z', sessionId: 's1', tool: 'copilot', cwd: '', dataHomeKey: 'deadbeefdeadbeef' };
+
+ const workspaces = await dashboardWorkspaces([copilot]);
+ const unassigned = workspaces.find(w => w.id === 'unassigned');
+ expect(unassigned, JSON.stringify(workspaces)).toBeDefined();
+ expect(unassigned && workspaceEvents([copilot], unassigned, workspaces).map(e => e.sessionId)).toEqual(['s1']);
+ });
});
diff --git a/src/dashboard/client.ts b/src/dashboard/client.ts
index 5b7067293..b1da5b7a1 100644
--- a/src/dashboard/client.ts
+++ b/src/dashboard/client.ts
@@ -177,10 +177,19 @@ function distribution() {
+(previewError?''+e(previewError)+'':'')+'';
return top+rolesPanel+projectsPanel+actions+previewPanel;
}
-async function previewSelection() { if(!dist||!distDraft)return; const generation=workspaceGeneration, version=distVersion; previewing=true;previewError='';saveError='';savedNote='';render(); try{ const data=await postJson('/api/distribution/preview',{primaryRole:distDraft.primary,additionalRoles:distDraft.extra,projects:distDraft.projects}); if(generation!==workspaceGeneration||version!==distVersion)return; preview=data; } catch(error){ if(generation!==workspaceGeneration||version!==distVersion)return; preview=null; previewError=error.message; } finally{ if(generation===workspaceGeneration){ previewing=false; if(page==='distribution')render(); } } }
-async function saveSelection() { if(!dist||!distDraft||!preview)return; const generation=workspaceGeneration, version=distVersion; saving=true;saveError='';savedNote='';render(); try{ let revision=dist.revision; if(dist.roles.state==='ok'){ const fresh=await postJson('/api/distribution/roles',{primaryRole:distDraft.primary,additionalRoles:distDraft.extra,revision}); // A workspace switch or draft edit mid-save must stop the chain before
+// Only the halves the team defines and the draft changed: a team with no
+// projects manifest (or no roles manifest) refuses a body that names them, and
+// a workspace with no primary role yet can still change its projects alone.
+function selectionChange() {
+ const sel=dist.selection, body={}, same=(a,b)=>JSON.stringify([...a].sort())===JSON.stringify([...b].sort());
+ if(dist.roles.state==='ok'&&(distDraft.primary!==sel.primaryRole||!same(distDraft.extra,sel.additionalRoles))){ if(!distDraft.primary)return null; body.primaryRole=distDraft.primary; body.additionalRoles=distDraft.extra; }
+ if(dist.projects.state==='ok'&&!same(distDraft.projects,sel.projects))body.projects=distDraft.projects;
+ return body;
+}
+async function previewSelection() { if(!dist||!distDraft)return; const generation=workspaceGeneration, version=distVersion; const change=selectionChange(); if(!change){ preview=null; previewError=t('Pick a primary role first.'); render(); return; } previewing=true;previewError='';saveError='';savedNote='';render(); try{ const data=await postJson('/api/distribution/preview',change); if(generation!==workspaceGeneration||version!==distVersion)return; preview=data; } catch(error){ if(generation!==workspaceGeneration||version!==distVersion)return; preview=null; previewError=error.message; } finally{ if(generation===workspaceGeneration){ previewing=false; if(page==='distribution')render(); } } }
+async function saveSelection() { if(!dist||!distDraft||!preview)return; const generation=workspaceGeneration, version=distVersion; saving=true;saveError='';savedNote='';render(); try{ const change=selectionChange()||{}; let revision=dist.revision; if('primaryRole' in change){ const fresh=await postJson('/api/distribution/roles',{primaryRole:distDraft.primary,additionalRoles:distDraft.extra,revision}); // A workspace switch or draft edit mid-save must stop the chain before
// the second write: the revision belongs to the old workspace/selection.
- if(generation!==workspaceGeneration)return; if(version!==distVersion){ saveError='The selection changed while saving. Preview again to save the rest.'; return; } revision=fresh.revision; } if(dist.projects.state==='ok')await postJson('/api/distribution/projects',{projects:distDraft.projects,revision}); if(generation!==workspaceGeneration||version!==distVersion)return; savedNote='Saved. Takes effect on the next pull.'; } catch(error){ if(generation!==workspaceGeneration)return; saveError=error.message; } finally{ if(generation===workspaceGeneration){ saving=false; const note=savedNote, err=saveError; await loadDistribution(); savedNote=note; saveError=err; if(page==='distribution')render(); } } }
+ if(generation!==workspaceGeneration)return; if(version!==distVersion){ saveError='The selection changed while saving. Preview again to save the rest.'; return; } revision=fresh.revision; } if('projects' in change)await postJson('/api/distribution/projects',{projects:distDraft.projects,revision}); if(generation!==workspaceGeneration||version!==distVersion)return; savedNote='Saved. Takes effect on the next pull.'; } catch(error){ if(generation!==workspaceGeneration)return; saveError=error.message; } finally{ if(generation===workspaceGeneration){ saving=false; const note=savedNote, err=saveError; await loadDistribution(); savedNote=note; saveError=err; loadRules(); loadSkills(); if(page==='distribution')render(); } } }
// Translate static report labels only; never translate knowledge titles, authors, commands or session text.
function localizeReport() {
const root=$('kb-content');if(!root)return;
@@ -324,6 +333,8 @@ async function toggleSkillExclude(name, excluded) {
try {
const response=await fetch(api('/api/resources/skills/exclusions'),{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({name,excluded})});
if(!response.ok)throw new Error('exclude failed');
+ // What the Distribution page counts as delivered changed too; it reloads on next view.
+ if(generation===workspaceGeneration&&!distLoading)dist=null;
await loadSkills();
} catch { if(generation===workspaceGeneration)skillsError=true; } finally { if(generation===workspaceGeneration){skillBusy='';if(page==='resources')render();} }
}
diff --git a/src/dashboard/locales.ts b/src/dashboard/locales.ts
index 85c2405ec..b6a38b3f3 100644
--- a/src/dashboard/locales.ts
+++ b/src/dashboard/locales.ts
@@ -235,6 +235,7 @@ export const dashboardMessages: Record = {
"Saved. Takes effect on the next pull.": "已保存。将在下次 pull 时生效。",
"Preview for the next pull": "下次 pull 的预览",
"Nothing is written until you save": "保存前不会写入任何内容",
+ "Pick a primary role first.": "请先选择一个主角色。",
"No change": "无变化",
"entering": "进入",
"Pull keeps what is installed until the copies are made equal or renamed.": "冲突解决前,pull 保留已安装的副本;请将两份副本改为一致或重命名其中之一。",
diff --git a/src/dashboard/workspaces.ts b/src/dashboard/workspaces.ts
index 14d392cc6..7b296652d 100644
--- a/src/dashboard/workspaces.ts
+++ b/src/dashboard/workspaces.ts
@@ -98,10 +98,11 @@ export async function dashboardWorkspaces(events: DashboardEvent[]): Promise w.scope === 'project');
- if (events.some(event => event.cwd && !ownerOf(event, projects))) {
- result.push({ id: 'unassigned', label: 'Unassigned sessions', scope: 'unassigned', root: '', config: null, roots: [] });
- }
+ // A session is unassigned when none of its events finds a project, the same
+ // rule workspaceEvents() routes by: a Copilot session records no cwd, and
+ // checking only events with one hid it from every workspace (#1121).
+ const unassigned: DashboardWorkspace = { id: 'unassigned', label: 'Unassigned sessions', scope: 'unassigned', root: '', config: null, roots: [] };
+ if (workspaceEvents(events, unassigned, result).length) result.push(unassigned);
return result;
}
export function workspaceEvents(events: DashboardEvent[], workspace: DashboardWorkspace, workspaces: DashboardWorkspace[]): DashboardEvent[] {