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[] {