@@ -50,7 +50,8 @@ export type Action =
50 | { type: "scheduleDissolve"; groupId: number; delayMs: number }
50 | { type: "scheduleDissolve"; groupId: number; delayMs: number }
51 | { type: "dirty"; groupId: number }
51 | { type: "dirty"; groupId: number }
52 | { type: "loose"; windowId: number }
52 | { type: "loose"; windowId: number }
53− | { type: "unpark"; tabId: number; groupId: number };
53+ | { type: "unpark"; tabId: number; groupId: number }
54+ | { type: "checkFit" };
55export interface Ctx {
56export interface Ctx {
56 now: number;
57 now: number;
@@ -105,6 +106,18 @@ const handlers: Handlers = {
105 if (pathChanged) rec.keepLoose = undefined; // a new page is fair game again
106 if (pathChanged) rec.keepLoose = undefined; // a new page is fair game again
106 if (before.groupId !== rec.groupId) {
107 if (before.groupId !== rec.groupId) {
107 left(s, before.groupId, ctx, out);
108 left(s, before.groupId, ctx, out);
109+ rec.fitPending = undefined;
110+ if (rec.groupId !== -1) {
111+ // Into a group: ours if the worker put it there, otherwise the user did and it stays.
112+ if (s.ownAdds[rec.id] !== undefined) {
113+ delete s.ownAdds[rec.id];
114+ rec.handPlaced = undefined;
116+ rec.handPlaced = true;
119+ rec.handPlaced = undefined;
108 if (rec.groupId !== -1) dirty(s, rec.groupId, ctx, out);
121 if (rec.groupId !== -1) dirty(s, rec.groupId, ctx, out);
109 else if (before.groupId !== -1) {
122 else if (before.groupId !== -1) {
110 // Out of a group: ours if the worker dissolved it, otherwise the user's choice to keep it loose.
123 // Out of a group: ours if the worker dissolved it, otherwise the user's choice to keep it loose.
@@ -122,6 +135,8 @@ const handlers: Handlers = {
122 if (titleChanged || pathChanged || loaded || before.pinned !== rec.pinned) loose(rec, ctx, out);
135 if (titleChanged || pathChanged || loaded || before.pinned !== rec.pinned) loose(rec, ctx, out);
138+ if (pathChanged && fitCheckable(s, rec, ctx.settings)) rec.fitPending = true;
139+ if (rec.fitPending && rec.status === "complete") out.push({ type: "checkFit" });
125 if (titleChanged || pathChanged) dirty(s, rec.groupId, ctx, out);
140 if (titleChanged || pathChanged) dirty(s, rec.groupId, ctx, out);
126 else if (before.status !== "complete" && rec.status === "complete" && s.groups[rec.groupId]?.dirty) {
141 else if (before.status !== "complete" && rec.status === "complete" && s.groups[rec.groupId]?.dirty) {
127 // A member finished loading: the naming loop may have been waiting for it.
142 // A member finished loading: the naming loop may have been waiting for it.
@@ -140,6 +155,8 @@ const handlers: Handlers = {
140 const rec = s.tabs[tabId];
155 const rec = s.tabs[tabId];
141 if (!rec) return;
156 if (!rec) return;
142 rec.lastActivatedAt = ctx.now;
157 rec.lastActivatedAt = ctx.now;
158+ // A tab waiting for its fit check is checked once the user has moved on from it.
159+ if (Object.values(s.tabs).some((t) => t.fitPending && t.id !== tabId)) out.push({ type: "checkFit" });
143 // Using a parked tab means it is not stale: it leaves Parked and auto-organize places it.
160 // Using a parked tab means it is not stale: it leaves Parked and auto-organize places it.
144 if (rec.groupId !== -1 && s.groups[rec.groupId]?.origin === "tidy") {
161 if (rec.groupId !== -1 && s.groups[rec.groupId]?.origin === "tidy") {
145 rec.parkedFrom = undefined;
162 rec.parkedFrom = undefined;
@@ -236,6 +253,21 @@ function openerRule(s: State, child: TabSnapshot, opener: TabSnapshot | undefine
236export const shouldDissolve = (g: GroupRecord, settings: Settings): boolean =>
253export const shouldDissolve = (g: GroupRecord, settings: Settings): boolean =>
237 g.managed && (g.origin === "opener" || g.origin === "organize") && !g.userNamed && settings.dissolveSingletons;
254 g.managed && (g.origin === "opener" || g.origin === "organize") && !g.userNamed && settings.dissolveSingletons;
256+/** Only groups Diagonal made and still names itself, and only tabs it put there, are checked for fit. */
257+export function fitCheckable(s: State, rec: TabRecord, settings: Settings): boolean {
258+ const g = s.groups[rec.groupId];
260+ settings.autoOrganize &&
263+ (g.origin === "opener" || g.origin === "organize") &&
267+ !isInternalUrl(rec.url)
239/** An ungrouped tab changed: auto-organize should look at its window once things settle. */
271/** An ungrouped tab changed: auto-organize should look at its window once things settle. */
240function loose(rec: TabRecord, ctx: Ctx, out: Action[]): void {
272function loose(rec: TabRecord, ctx: Ctx, out: Action[]): void {
241 if (!ctx.settings.autoOrganize || rec.groupId !== -1 || rec.pinned || rec.keepLoose || isInternalUrl(rec.url)) return;
273 if (!ctx.settings.autoOrganize || rec.groupId !== -1 || rec.pinned || rec.keepLoose || isInternalUrl(rec.url)) return;
@@ -305,6 +337,8 @@ function upsertTab(s: State, tab: TabSnapshot, now: number): TabRecord {
305 if (before?.description && pathKey(before.url) === pathKey(url)) rec.description = before.description;
337 if (before?.description && pathKey(before.url) === pathKey(url)) rec.description = before.description;
306 if (before?.keepLoose) rec.keepLoose = before.keepLoose;
338 if (before?.keepLoose) rec.keepLoose = before.keepLoose;
307 if (before?.organizedKey) rec.organizedKey = before.organizedKey;
339 if (before?.organizedKey) rec.organizedKey = before.organizedKey;
340+ if (before?.handPlaced) rec.handPlaced = true;
341+ if (before?.fitPending) rec.fitPending = true;
308 s.tabs[tab.id] = rec;
342 s.tabs[tab.id] = rec;
309 return rec;
343 return rec;
@@ -327,4 +361,5 @@ function prune(s: State, now: number): void {
327 s.pendingCreates = s.pendingCreates.filter((p) => now - p.at <= OWN_CREATE_WINDOW_MS);
361 s.pendingCreates = s.pendingCreates.filter((p) => now - p.at <= OWN_CREATE_WINDOW_MS);
328 for (const [id, w] of Object.entries(s.ownWrites)) if (now - w.at > OWN_WRITE_WINDOW_MS) delete s.ownWrites[+id];
362 for (const [id, w] of Object.entries(s.ownWrites)) if (now - w.at > OWN_WRITE_WINDOW_MS) delete s.ownWrites[+id];
329 for (const [id, at] of Object.entries(s.ownUngroups)) if (now - at > OWN_WRITE_WINDOW_MS) delete s.ownUngroups[+id];
363 for (const [id, at] of Object.entries(s.ownUngroups)) if (now - at > OWN_WRITE_WINDOW_MS) delete s.ownUngroups[+id];
364+ for (const [id, at] of Object.entries(s.ownAdds)) if (now - at > OWN_WRITE_WINDOW_MS) delete s.ownAdds[+id];