Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions src/__tests__/dashboard-workspaces.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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']);
});
});
17 changes: 14 additions & 3 deletions src/dashboard/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -177,10 +177,19 @@ function distribution() {
+(previewError?'<span class="amber">'+e(previewError)+'</span>':'')+'</div>';
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;
Expand Down Expand Up @@ -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();} }
}
Expand Down
1 change: 1 addition & 0 deletions src/dashboard/locales.ts
Original file line number Diff line number Diff line change
Expand Up @@ -235,6 +235,7 @@ export const dashboardMessages: Record<string, string> = {
"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 保留已安装的副本;请将两份副本改为一致或重命名其中之一。",
Expand Down
9 changes: 5 additions & 4 deletions src/dashboard/workspaces.ts
Original file line number Diff line number Diff line change
Expand Up @@ -98,10 +98,11 @@ export async function dashboardWorkspaces(events: DashboardEvent[]): Promise<Das
// Sessions whose cwd matches no installed project no longer fold into User scope
// (that silently inflated it). Surface them in a dedicated "unassigned" bucket,
// but only when such sessions actually exist (PR #604 review #3).
const projects = result.filter(w => 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[] {
Expand Down
Loading