Skip to content
Merged
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/main/config-store.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
import Store from 'electron-store';
import type { PaneSummaryConfig } from '../shared/pane-summary-types';
import { DEFAULT_PANE_SUMMARY_CONFIG } from '../shared/pane-summary-types';

export interface ShellProfile {
id: string;
Expand Down Expand Up @@ -109,6 +111,14 @@ export interface AppConfig {
* ('alpha'). (TASK-135)
*/
aiGroupByRepoOrder?: 'activity' | 'alpha';
/**
* AI-distilled 1-line pane summary feature (Task pane-summary). When
* `enabled` (default true), tmax spawns a sandboxed `copilot -p` per
* AI pane after `delayMs` and ≥3 user turns, and surfaces the result
* on hover of the tab/pane title. v1 = Copilot only; Claude panes
* report `unavailable`. The result is in-memory only, not persisted.
*/
paneSummary?: PaneSummaryConfig;
}

function findPwsh(): string | null {
Expand Down Expand Up @@ -266,6 +276,7 @@ export const defaultConfig: AppConfig = {
notificationExcludeStrings: [],
aiShimmerEnabled: true,
aiSessionLoadLimit: 314,
paneSummary: DEFAULT_PANE_SUMMARY_CONFIG,
};

export class ConfigStore {
Expand Down
23 changes: 23 additions & 0 deletions src/main/copilot-session-monitor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import * as os from 'node:os';
import { parseSessionEvents, clearParserCache, extractCopilotPrompts } from './copilot-events-parser';
import { CopilotSessionDB, sessionRowToSummary } from './copilot-session-db';
import { tokenizeAnd, matchesAllTokens } from '../shared/and-filter';
import { SUMMARIZER_SESSION_NAME_PREFIX } from '../shared/pane-summary-types';
import type {
CopilotSession,
CopilotSessionSummary,
Expand Down Expand Up @@ -50,6 +51,13 @@ export class CopilotSessionMonitor {
return this.basePath;
}

/** Returns true when a session row from SQLite or filesystem belongs to
* the pane-summary feature's sandboxed summarizer spawn. Filtered out
* of all UI surfaces. See src/main/pane-summary-service.ts. */
private isSummarizerSessionRow(row: { summary?: string }): boolean {
return !!row.summary && row.summary.startsWith(SUMMARIZER_SESSION_NAME_PREFIX);
}

async scanSessions(limit = 314): Promise<CopilotSessionSummary[]> {
// Try SQLite first (9-15x faster than filesystem scan)
if (this.dbAvailable === null) {
Expand Down Expand Up @@ -89,6 +97,10 @@ export class CopilotSessionMonitor {
const currentIds = new Set<string>();

for (const row of sessionRows) {
// Skip our own summarizer-spawned sessions (Task pane-summary).
// They appear with `summary: tmax-summarizer:<uuid>` because the
// CLI mirrors `-n` into the DB summary field.
if (this.isSummarizerSessionRow(row)) continue;
currentIds.add(row.id);
const turnStats = turnStatsMap.get(row.id);
const summary = sessionRowToSummary(row, turnStats);
Expand Down Expand Up @@ -206,6 +218,11 @@ export class CopilotSessionMonitor {
const session = this.loadSession(sessionId, sessionDir);

if (session) {
// Skip our own summarizer-spawned sessions (Task pane-summary).
if (session.workspace?.name?.startsWith(SUMMARIZER_SESSION_NAME_PREFIX)
|| session.workspace?.summary?.startsWith(SUMMARIZER_SESSION_NAME_PREFIX)) {
continue;
}
this.sessions.set(sessionId, session);
const summary = this.toSummary(session);
summaries.push(summary);
Expand Down Expand Up @@ -295,6 +312,7 @@ export class CopilotSessionMonitor {

const results: CopilotSessionSummary[] = [];
for (const row of searchRows) {
if (this.isSummarizerSessionRow(row)) continue;
const turnStats = turnStatsMap?.get(row.id);
const summary = sessionRowToSummary(row, turnStats);

Expand Down Expand Up @@ -394,6 +412,11 @@ export class CopilotSessionMonitor {
}
const session = this.loadSession(sessionId, sessionDir);
if (session) {
// Skip our own summarizer sessions (Task pane-summary).
if (session.workspace?.name?.startsWith(SUMMARIZER_SESSION_NAME_PREFIX)
|| session.workspace?.summary?.startsWith(SUMMARIZER_SESSION_NAME_PREFIX)) {
return;
}
this.sessions.set(sessionId, session);
this.callbacks.onSessionAdded?.(this.toSummary(session));
}
Expand Down
33 changes: 33 additions & 0 deletions src/main/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import { KeybindingsFile } from './keybindings-file';
import { IPC } from '../shared/ipc-channels';
import { CopilotSessionMonitor } from './copilot-session-monitor';
import { CopilotSessionWatcher } from './copilot-session-watcher';
import { PaneSummaryService } from './pane-summary-service';
import { notifyCopilotSession, clearNotificationCooldowns, setAiSessionNotificationsEnabled, setNotificationClickHandler, setSessionNameOverrides, setNotificationExcludeStrings } from './copilot-notification';
import { ClaudeCodeSessionMonitor } from './claude-code-session-monitor';
import { ClaudeCodeSessionWatcher } from './claude-code-session-watcher';
Expand Down Expand Up @@ -133,6 +134,7 @@ let configStore: ConfigStore | null = null;
let keybindingsFile: KeybindingsFile | null = null;
let copilotMonitor: CopilotSessionMonitor | null = null;
let copilotWatcher: CopilotSessionWatcher | null = null;
let paneSummaryService: PaneSummaryService | null = null;
let claudeCodeMonitor: ClaudeCodeSessionMonitor | null = null;
let claudeCodeWatcher: ClaudeCodeSessionWatcher | null = null;
let wslSessionManager: WslSessionManager | null = null;
Expand Down Expand Up @@ -620,6 +622,29 @@ function registerIpcHandlers(): void {
return await getDescendantNames(pid);
});

// ── Pane summary (Task pane-summary). Routes renderer requests to
// the PaneSummaryService (lazily instantiated once the copilot monitor
// is up). Falls back to an `unavailable` error if the service couldn't
// be created (e.g. monitor never initialised).
ipcMain.on(IPC.PANE_SUMMARY_REQUEST, (event, req: import('../shared/pane-summary-types').PaneSummaryRequest) => {
diagLog('paneSummary.request', {
terminalId: sanitize(req?.terminalId),
provider: req?.provider,
force: !!req?.force,
});
if (!paneSummaryService) {
event.sender.send(IPC.PANE_SUMMARY_ERROR, {
terminalId: req.terminalId,
sessionId: req.sessionId,
provider: req.provider,
message: 'pane summary service not initialised',
unavailable: true,
});
return;
}
paneSummaryService.request(req, event.sender);
});

ipcMain.on(IPC.DIAG_LOG, (_event, event: string, data?: Record<string, unknown>) => {
diagLog(event, data);
});
Expand Down Expand Up @@ -1236,6 +1261,12 @@ function registerIpcHandlers(): void {
function setupCopilotMonitor(): void {
copilotMonitor = new CopilotSessionMonitor();

// Lazily wire pane-summary service now that the monitor exists.
paneSummaryService = new PaneSummaryService({
monitor: copilotMonitor,
config: () => configStore?.get('paneSummary'),
});

copilotMonitor.setCallbacks({
onSessionUpdated(session) {
mainWindow?.webContents.send(IPC.COPILOT_SESSION_UPDATED, session);
Expand Down Expand Up @@ -1546,6 +1577,8 @@ app.on('window-all-closed', async () => {
sessionFileWatcher = null;
await copilotWatcher?.stop();
copilotMonitor?.dispose();
paneSummaryService?.dispose();
paneSummaryService = null;
await claudeCodeWatcher?.stop();
claudeCodeMonitor?.dispose();
await wslSessionManager?.stop();
Expand Down
Loading