macos task switch focus

This commit is contained in:
Ian Gulliver
2026-10-17 09:04:32 -07:00
parent ebf1561c4a
commit c42a513e2c
14 changed files with 68 additions and 12 deletions
+1 -1
View File
File diff suppressed because one or more lines are too long
+6 -2
View File
@@ -170,8 +170,12 @@ final class ChatModel {
} }
} }
func takeFocusStart() -> Bool { func takeFocus() -> Bool {
let take = focusStart && session == nil let asked = taskId != nil && store.composerFocus == taskId
if asked {
store.composerFocus = nil
}
let take = asked || (focusStart && session == nil)
focusStart = false focusStart = false
return take return take
} }
+6 -1
View File
@@ -332,7 +332,12 @@ struct ComposerView: View {
} }
.onAppear { .onAppear {
text = store.drafts?.composerText(draftKey) ?? "" text = store.drafts?.composerText(draftKey) ?? ""
if chat.takeFocusStart() { if chat.takeFocus() {
focused = true
}
}
.onChange(of: store.composerFocus) {
if chat.takeFocus() {
focused = true focused = true
} }
} }
+1
View File
@@ -41,6 +41,7 @@ final class Store {
var leadSessions: [AgentSession] = [] var leadSessions: [AgentSession] = []
let leadFeed: Feed let leadFeed: Feed
var jump: (taskId: String, seq: Int64)? var jump: (taskId: String, seq: Int64)?
var composerFocus: String?
var summaryReads: [String: Int64] = [:] var summaryReads: [String: Int64] = [:]
private(set) var leadMore = false private(set) var leadMore = false
var todoStarting: Set<String> = [] var todoStarting: Set<String> = []
+2
View File
@@ -260,8 +260,10 @@ final class SwitcherModel {
if store.inbox.contains(where: { $0.taskId == taskId }) { if store.inbox.contains(where: { $0.taskId == taskId }) {
store.focusTask(taskId, .inbox) store.focusTask(taskId, .inbox)
} else if let task = store.findTask(taskId), store.unreadSummary(task) { } else if let task = store.findTask(taskId), store.unreadSummary(task) {
store.composerFocus = taskId
store.openSummary(task) store.openSummary(task)
} else { } else {
store.composerFocus = taskId
store.focusTask(taskId, .transcript) store.focusTask(taskId, .transcript)
} }
} }
+5
View File
@@ -16,6 +16,7 @@ export const state = {
leadSessions: [], leadSessions: [],
leadTranscript: [], leadTranscript: [],
jump: undefined, jump: undefined,
composerFocus: undefined,
summaryReads: {}, summaryReads: {},
}; };
const listeners = new Map(); const listeners = new Map();
@@ -359,6 +360,10 @@ export function openSummary(task) {
focusTask(task.id, "transcript"); focusTask(task.id, "transcript");
notify("jump"); notify("jump");
} }
export function focusComposer(taskId) {
state.composerFocus = taskId;
notify("composer");
}
export function selectTask(taskId) { export function selectTask(taskId) {
markVisited(state.selectedTaskId); markVisited(state.selectedTaskId);
markVisited(taskId); markVisited(taskId);
+1 -1
View File
File diff suppressed because one or more lines are too long
+3 -1
View File
@@ -2,7 +2,7 @@ import { get, post } from "./api.js";
import { errorText, h, replace, showAlert } from "./dom.js"; import { errorText, h, replace, showAlert } from "./dom.js";
import { focusLead } from "./panes.js"; import { focusLead } from "./panes.js";
import { summaryMark } from "./project.js"; import { summaryMark } from "./project.js";
import { activeTasks, findTask, focusedProject, focusProject, focusTab, focusTask, lastVisits, onEach, openSummary, state, taskState, unreadSummary } from "./state.js"; import { activeTasks, findTask, focusComposer, focusedProject, focusProject, focusTab, focusTask, lastVisits, onEach, openSummary, state, taskState, unreadSummary } from "./state.js";
import { confirmMerge, models } from "./task.js"; import { confirmMerge, models } from "./task.js";
import { taskStyle } from "./taskcolor.js"; import { taskStyle } from "./taskcolor.js";
export function bindShortcuts() { export function bindShortcuts() {
@@ -122,9 +122,11 @@ export function openSwitcher() {
focusTask(taskId, "inbox"); focusTask(taskId, "inbox");
} }
else if (task && unreadSummary(task)) { else if (task && unreadSummary(task)) {
focusComposer(taskId);
openSummary(task); openSummary(task);
} }
else { else {
focusComposer(taskId);
focusTask(taskId, "transcript"); focusTask(taskId, "transcript");
} }
}; };
+1 -1
View File
File diff suppressed because one or more lines are too long
+15 -1
View File
@@ -280,6 +280,14 @@ export function mountChat(root, src, makeHead) {
}; };
let composerModel = ""; let composerModel = "";
let focusStart = false; let focusStart = false;
const takeFocus = () => {
const taskId = src.taskId();
if (!taskId || state.composerFocus !== taskId) {
return false;
}
state.composerFocus = undefined;
return true;
};
const renderComposer = () => { const renderComposer = () => {
const key = src.key(); const key = src.key();
const old = composer.querySelector("textarea"); const old = composer.querySelector("textarea");
@@ -297,11 +305,12 @@ export function mountChat(root, src, makeHead) {
const caret = old !== null && document.activeElement === old ? [old.selectionStart, old.selectionEnd] : undefined; const caret = old !== null && document.activeElement === old ? [old.selectionStart, old.selectionEnd] : undefined;
replace(composer, form); replace(composer, form);
const text = form.querySelector("textarea"); const text = form.querySelector("textarea");
const asked = takeFocus();
if (caret) { if (caret) {
text.focus(); text.focus();
text.setSelectionRange(caret[0], caret[1]); text.setSelectionRange(caret[0], caret[1]);
} }
else if (focusStart && !sess) { else if (asked || (focusStart && !sess)) {
text.focus(); text.focus();
} }
focusStart = false; focusStart = false;
@@ -642,6 +651,11 @@ export function mountChat(root, src, makeHead) {
syncMergeAfterReview(transcript); syncMergeAfterReview(transcript);
}), }),
on("jump", jump), on("jump", jump),
on("composer", () => {
if (takeFocus()) {
composer.querySelector("textarea")?.focus();
}
}),
]; ];
replace(root, h("div", { class: "transcript-view" }, head, transcript, composer)); replace(root, h("div", { class: "transcript-view" }, head, transcript, composer));
renderAll(); renderAll();
+1 -1
View File
File diff suppressed because one or more lines are too long
+8 -1
View File
@@ -21,7 +21,8 @@ export type Topic =
| "ready" | "ready"
| "lead.ready" | "lead.ready"
| "summaries" | "summaries"
| "jump"; | "jump"
| "composer";
export const tabs: TaskTab[] = ["transcript", "changes", "files", "terminal", "inbox", "reviews", "workflow", "project"]; export const tabs: TaskTab[] = ["transcript", "changes", "files", "terminal", "inbox", "reviews", "workflow", "project"];
@@ -40,6 +41,7 @@ export const state = {
leadSessions: [] as AgentSession[], leadSessions: [] as AgentSession[],
leadTranscript: [] as ByreEvent[], leadTranscript: [] as ByreEvent[],
jump: undefined as { taskId: string; seq: number } | undefined, jump: undefined as { taskId: string; seq: number } | undefined,
composerFocus: undefined as string | undefined,
summaryReads: {} as Record<string, number>, summaryReads: {} as Record<string, number>,
}; };
@@ -433,6 +435,11 @@ export function openSummary(task: Task): void {
notify("jump"); notify("jump");
} }
export function focusComposer(taskId: string): void {
state.composerFocus = taskId;
notify("composer");
}
export function selectTask(taskId: string | undefined): void { export function selectTask(taskId: string | undefined): void {
markVisited(state.selectedTaskId); markVisited(state.selectedTaskId);
markVisited(taskId); markVisited(taskId);
+3 -1
View File
@@ -2,7 +2,7 @@ import { get, post, type Project, type WorkflowSummary } from "./api.js";
import { errorText, h, replace, showAlert } from "./dom.js"; import { errorText, h, replace, showAlert } from "./dom.js";
import { focusLead } from "./panes.js"; import { focusLead } from "./panes.js";
import { summaryMark } from "./project.js"; import { summaryMark } from "./project.js";
import { activeTasks, findTask, focusedProject, focusProject, focusTab, focusTask, lastVisits, onEach, openSummary, state, taskState, unreadSummary } from "./state.js"; import { activeTasks, findTask, focusComposer, focusedProject, focusProject, focusTab, focusTask, lastVisits, onEach, openSummary, state, taskState, unreadSummary } from "./state.js";
import { confirmMerge, models } from "./task.js"; import { confirmMerge, models } from "./task.js";
import { taskStyle } from "./taskcolor.js"; import { taskStyle } from "./taskcolor.js";
@@ -131,8 +131,10 @@ export function openSwitcher(): void {
if (state.inbox.some((item) => item.task_id === taskId)) { if (state.inbox.some((item) => item.task_id === taskId)) {
focusTask(taskId, "inbox"); focusTask(taskId, "inbox");
} else if (task && unreadSummary(task)) { } else if (task && unreadSummary(task)) {
focusComposer(taskId);
openSummary(task); openSummary(task);
} else { } else {
focusComposer(taskId);
focusTask(taskId, "transcript"); focusTask(taskId, "transcript");
} }
}; };
+15 -1
View File
@@ -360,6 +360,14 @@ export function mountChat(root: HTMLElement, src: ChatSource, makeHead: (rerende
let composerModel = ""; let composerModel = "";
let focusStart = false; let focusStart = false;
const takeFocus = () => {
const taskId = src.taskId();
if (!taskId || state.composerFocus !== taskId) {
return false;
}
state.composerFocus = undefined;
return true;
};
const renderComposer = () => { const renderComposer = () => {
const key = src.key(); const key = src.key();
const old = composer.querySelector("textarea"); const old = composer.querySelector("textarea");
@@ -377,10 +385,11 @@ export function mountChat(root: HTMLElement, src: ChatSource, makeHead: (rerende
const caret = old !== null && document.activeElement === old ? [old.selectionStart, old.selectionEnd] : undefined; const caret = old !== null && document.activeElement === old ? [old.selectionStart, old.selectionEnd] : undefined;
replace(composer, form); replace(composer, form);
const text = form.querySelector("textarea")!; const text = form.querySelector("textarea")!;
const asked = takeFocus();
if (caret) { if (caret) {
text.focus(); text.focus();
text.setSelectionRange(caret[0]!, caret[1]!); text.setSelectionRange(caret[0]!, caret[1]!);
} else if (focusStart && !sess) { } else if (asked || (focusStart && !sess)) {
text.focus(); text.focus();
} }
focusStart = false; focusStart = false;
@@ -740,6 +749,11 @@ export function mountChat(root: HTMLElement, src: ChatSource, makeHead: (rerende
syncMergeAfterReview(transcript); syncMergeAfterReview(transcript);
}), }),
on("jump", jump), on("jump", jump),
on("composer", () => {
if (takeFocus()) {
composer.querySelector("textarea")?.focus();
}
}),
]; ];
replace(root, h("div", { class: "transcript-view" }, head, transcript, composer)); replace(root, h("div", { class: "transcript-view" }, head, transcript, composer));