Files
aiturk-hermes-ide/apps/desktop/electron/quit-guard.ts
T

97 lines
3.2 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// Quitting with a turn in flight kills the backend mid-tool-call: the work is
// lost, and anything the agent had half-written to disk stays half-written.
// Renderers publish what they're running; the main process asks before it lets
// that go. The decision + copy live here (pure, testable) so main.ts only owns
// the IPC and the dialog call.
const MAX_LISTED = 4
export interface ActiveWork {
/** Titles of sessions running a turn. Untitled sessions contribute a count only. */
titles: string[]
/** Running turns, including untitled ones — always >= titles.length. */
count: number
}
export const NO_ACTIVE_WORK: ActiveWork = { count: 0, titles: [] }
/** Coerce an IPC payload from an untrusted renderer into an ActiveWork. */
export function normalizeActiveWork(payload: unknown): ActiveWork {
if (!payload || typeof payload !== 'object') {
return NO_ACTIVE_WORK
}
const raw = payload as { count?: unknown; titles?: unknown }
const titles = Array.isArray(raw.titles)
? raw.titles
.filter((title): title is string => typeof title === 'string')
.map(title => title.trim())
.filter(Boolean)
: []
const count = typeof raw.count === 'number' && Number.isFinite(raw.count) ? Math.max(0, Math.floor(raw.count)) : 0
return { count: Math.max(count, titles.length), titles }
}
/** Merge every window's report into one. Windows can show the same session. */
export function mergeActiveWork(reports: Iterable<ActiveWork>): ActiveWork {
const titles: string[] = []
let count = 0
for (const report of reports) {
count = Math.max(count, report.count)
for (const title of report.titles) {
if (!titles.includes(title)) {
titles.push(title)
}
}
}
return { count: Math.max(count, titles.length), titles }
}
export interface QuitPrompt {
detail: string
message: string
}
/**
* The confirmation to show, or null when quitting should just proceed.
*
* `quittingForHandoff` covers the update / swap / uninstall relaunches: those
* are the app replacing itself, not the user walking away, and a modal there
* would strand the detached script waiting on a PID that never exits.
*/
export function quitPromptFor(work: ActiveWork, quittingForHandoff: boolean, locale: 'en' | 'tr' = 'en'): null | QuitPrompt {
if (quittingForHandoff || work.count < 1) {
return null
}
const listed = work.titles.slice(0, MAX_LISTED)
const remaining = work.count - listed.length
const lines = listed.map(title => `• ${title}`)
if (remaining > 0) {
lines.push(locale === 'tr' ? `• ${remaining} sohbet daha` : remaining === 1 ? '• 1 more' : `• ${remaining} more`)
}
return {
detail: [
lines.join('\n'),
lines.length > 0 ? '' : null,
locale === 'tr'
? 'Çıkış yapmak asistanın devam eden çalışmasını durdurur. Henüz yazmayı tamamlamadığı çalışma kaybolur.'
: 'Quitting stops the agent mid-turn. Any work it has not finished writing is lost.'
]
.filter(line => line !== null)
.join('\n')
.trim(),
message: locale === 'tr'
? `AITURK hâlâ ${work.count} sohbet üzerinde çalışıyor.`
: work.count === 1 ? 'AITURK is still working on 1 chat.' : `AITURK is still working on ${work.count} chats.`
}
}