mirror of
https://github.com/jlengrand/Maestro.git
synced 2026-03-10 08:31:19 +00:00
MAESTRO: Tab switcher improvements and slash command UX refinements
- Add lastActivityAt timestamps to tab switcher for better session sorting - Filter "All Named" sessions to current project only (projectRoot-scoped) - Add relative time formatting (e.g., "5m ago", "2h ago") in tab switcher - Change slash command behavior: Tab/Enter now fills text instead of executing - Allow slash commands to be queued when agent is busy (like regular messages) - Add projectRoot field to Session for stable Claude session storage path - Fix markdown list indentation and positioning - Add GitHub Actions workflow for auto-assigning issues/PRs to pedramamini - Update CLAUDE.md with projectRoot field documentation Claude ID: 747fc9d0-a5a2-441d-bc0a-8cd3d579a004 Maestro ID: b9bc0d08-5be2-4fdf-93cd-5618a8d53b35
This commit is contained in:
25
.github/workflows/auto-assign.yml
vendored
Normal file
25
.github/workflows/auto-assign.yml
vendored
Normal file
@@ -0,0 +1,25 @@
|
||||
name: Auto Assign
|
||||
|
||||
on:
|
||||
issues:
|
||||
types: [opened]
|
||||
pull_request:
|
||||
types: [opened]
|
||||
|
||||
jobs:
|
||||
assign:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Assign to pedramamini
|
||||
uses: actions/github-script@v7
|
||||
with:
|
||||
script: |
|
||||
const issueNumber = context.issue?.number || context.payload.pull_request?.number;
|
||||
if (issueNumber) {
|
||||
await github.rest.issues.addAssignees({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
issue_number: issueNumber,
|
||||
assignees: ['pedramamini']
|
||||
});
|
||||
}
|
||||
@@ -172,7 +172,8 @@ interface Session {
|
||||
toolType: ToolType; // 'claude-code' | 'aider' | 'terminal' | etc.
|
||||
state: SessionState; // 'idle' | 'busy' | 'error' | 'connecting'
|
||||
inputMode: 'ai' | 'terminal'; // Which process receives input
|
||||
cwd: string; // Working directory
|
||||
cwd: string; // Current working directory (can change via cd)
|
||||
projectRoot: string; // Initial working directory (never changes, used for Claude session storage)
|
||||
aiPid: number; // AI process ID
|
||||
terminalPid: number; // Terminal process ID
|
||||
aiLogs: LogEntry[]; // AI output history
|
||||
|
||||
@@ -3187,23 +3187,40 @@ function setupIpcHandlers() {
|
||||
|
||||
// Get all named sessions across all projects (for Tab Switcher "All Named" view)
|
||||
ipcMain.handle('claude:getAllNamedSessions', async () => {
|
||||
const os = await import('os');
|
||||
const homeDir = os.default.homedir();
|
||||
const claudeProjectsDir = path.join(homeDir, '.claude', 'projects');
|
||||
|
||||
const allOrigins = claudeSessionOriginsStore.get('origins', {});
|
||||
const namedSessions: Array<{
|
||||
claudeSessionId: string;
|
||||
projectPath: string;
|
||||
sessionName: string;
|
||||
starred?: boolean;
|
||||
lastActivityAt?: number;
|
||||
}> = [];
|
||||
|
||||
for (const [projectPath, sessions] of Object.entries(allOrigins)) {
|
||||
for (const [claudeSessionId, info] of Object.entries(sessions)) {
|
||||
// Handle both old string format and new object format
|
||||
if (typeof info === 'object' && info.sessionName) {
|
||||
// Try to get last activity time from the session file
|
||||
let lastActivityAt: number | undefined;
|
||||
try {
|
||||
const encodedPath = encodeClaudeProjectPath(projectPath);
|
||||
const sessionFile = path.join(claudeProjectsDir, encodedPath, `${claudeSessionId}.jsonl`);
|
||||
const stats = await fs.stat(sessionFile);
|
||||
lastActivityAt = stats.mtime.getTime();
|
||||
} catch {
|
||||
// Session file may not exist or be inaccessible
|
||||
}
|
||||
|
||||
namedSessions.push({
|
||||
claudeSessionId,
|
||||
projectPath,
|
||||
sessionName: info.sessionName,
|
||||
starred: info.starred,
|
||||
lastActivityAt,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -418,6 +418,7 @@ contextBridge.exposeInMainWorld('maestro', {
|
||||
projectPath: string;
|
||||
sessionName: string;
|
||||
starred?: boolean;
|
||||
lastActivityAt?: number;
|
||||
}>>,
|
||||
deleteMessagePair: (projectPath: string, sessionId: string, userMessageUuid: string, fallbackContent?: string) =>
|
||||
ipcRenderer.invoke('claude:deleteMessagePair', projectPath, sessionId, userMessageUuid, fallbackContent),
|
||||
|
||||
@@ -337,6 +337,11 @@ export default function MaestroConsole() {
|
||||
// Restore a persisted session by respawning its process
|
||||
const restoreSession = async (session: Session): Promise<Session> => {
|
||||
try {
|
||||
// Migration: ensure projectRoot is set (for sessions created before this field was added)
|
||||
if (!session.projectRoot) {
|
||||
session = { ...session, projectRoot: session.cwd };
|
||||
}
|
||||
|
||||
// Sessions must have aiTabs - if missing, this is a data corruption issue
|
||||
if (!session.aiTabs || session.aiTabs.length === 0) {
|
||||
console.error('[restoreSession] Session has no aiTabs - data corruption, skipping:', session.id);
|
||||
@@ -3621,6 +3626,7 @@ export default function MaestroConsole() {
|
||||
state: 'idle',
|
||||
cwd: workingDir,
|
||||
fullPath: workingDir,
|
||||
projectRoot: workingDir, // Store the initial directory (never changes)
|
||||
isGitRepo,
|
||||
gitBranches,
|
||||
gitTags,
|
||||
@@ -3989,13 +3995,8 @@ export default function MaestroConsole() {
|
||||
return;
|
||||
}
|
||||
|
||||
// Block slash commands when agent is busy (in AI mode)
|
||||
if (effectiveInputValue.trim().startsWith('/') && activeSession.state === 'busy' && activeSession.inputMode === 'ai') {
|
||||
showFlashNotification('Commands disabled while agent is working');
|
||||
return;
|
||||
}
|
||||
|
||||
// Handle slash commands (custom AI commands only - built-in commands have been removed)
|
||||
// Note: slash commands are queued like regular messages when agent is busy
|
||||
if (effectiveInputValue.trim().startsWith('/')) {
|
||||
const commandText = effectiveInputValue.trim();
|
||||
const isTerminalMode = activeSession.inputMode === 'terminal';
|
||||
@@ -5073,152 +5074,13 @@ export default function MaestroConsole() {
|
||||
} else if (e.key === 'ArrowUp') {
|
||||
e.preventDefault();
|
||||
setSelectedSlashCommandIndex(prev => Math.max(prev - 1, 0));
|
||||
} else if (e.key === 'Tab') {
|
||||
// Tab just fills in the command text
|
||||
} else if (e.key === 'Tab' || e.key === 'Enter') {
|
||||
// Tab or Enter fills in the command text (user can then press Enter again to execute)
|
||||
e.preventDefault();
|
||||
setInputValue(filteredCommands[selectedSlashCommandIndex]?.command || inputValue);
|
||||
setSlashCommandOpen(false);
|
||||
} else if (e.key === 'Enter' && filteredCommands.length > 0) {
|
||||
// Enter executes the command directly
|
||||
e.preventDefault();
|
||||
const selectedCommand = filteredCommands[selectedSlashCommandIndex];
|
||||
if (selectedCommand) {
|
||||
if (filteredCommands[selectedSlashCommandIndex]) {
|
||||
setInputValue(filteredCommands[selectedSlashCommandIndex].command);
|
||||
setSlashCommandOpen(false);
|
||||
setInputValue('');
|
||||
if (inputRef.current) inputRef.current.style.height = 'auto';
|
||||
|
||||
// Execute the custom AI command (substitute template variables and send to agent)
|
||||
if ('prompt' in selectedCommand && selectedCommand.prompt) {
|
||||
// Use the same spawn logic as processInput for proper tab-based session ID tracking
|
||||
(async () => {
|
||||
let gitBranch: string | undefined;
|
||||
if (activeSession.isGitRepo) {
|
||||
try {
|
||||
const status = await gitService.getStatus(activeSession.cwd);
|
||||
gitBranch = status.branch;
|
||||
} catch {
|
||||
// Ignore git errors
|
||||
}
|
||||
}
|
||||
const substitutedPrompt = substituteTemplateVariables(
|
||||
selectedCommand.prompt,
|
||||
{ session: activeSession, gitBranch }
|
||||
);
|
||||
|
||||
// Get the active tab for proper targeting
|
||||
const activeTab = getActiveTab(activeSession);
|
||||
if (!activeTab) {
|
||||
console.error('[handleInputKeyDown] No active tab for slash command');
|
||||
return;
|
||||
}
|
||||
|
||||
// Build target session ID using tab ID (same pattern as processInput)
|
||||
const targetSessionId = `${activeSessionId}-ai-${activeTab.id}`;
|
||||
const isNewSession = !activeTab.claudeSessionId;
|
||||
|
||||
// Add user log showing the command with its interpolated prompt to active tab
|
||||
const newEntry: LogEntry = {
|
||||
id: generateId(),
|
||||
timestamp: Date.now(),
|
||||
source: 'user',
|
||||
text: substitutedPrompt,
|
||||
aiCommand: {
|
||||
command: selectedCommand.command,
|
||||
description: selectedCommand.description
|
||||
}
|
||||
};
|
||||
|
||||
// Update session state: add log, set busy, set awaitingSessionId for new sessions
|
||||
setSessions(prev => prev.map(s => {
|
||||
if (s.id !== activeSessionId) return s;
|
||||
|
||||
// Update the active tab's logs and state
|
||||
const updatedAiTabs = s.aiTabs.map(tab =>
|
||||
tab.id === activeTab.id
|
||||
? {
|
||||
...tab,
|
||||
logs: [...tab.logs, newEntry],
|
||||
state: 'busy' as const,
|
||||
thinkingStartTime: Date.now(),
|
||||
awaitingSessionId: isNewSession ? true : tab.awaitingSessionId
|
||||
}
|
||||
: tab
|
||||
);
|
||||
|
||||
return {
|
||||
...s,
|
||||
state: 'busy' as SessionState,
|
||||
busySource: 'ai',
|
||||
thinkingStartTime: Date.now(),
|
||||
currentCycleTokens: 0,
|
||||
currentCycleBytes: 0,
|
||||
aiCommandHistory: Array.from(new Set([...(s.aiCommandHistory || []), selectedCommand.command])).slice(-50),
|
||||
pendingAICommandForSynopsis: selectedCommand.command,
|
||||
aiTabs: updatedAiTabs
|
||||
};
|
||||
}));
|
||||
|
||||
// Spawn the agent with proper session ID format (same as processInput)
|
||||
try {
|
||||
const agent = await window.maestro.agents.get('claude-code');
|
||||
if (!agent) throw new Error('Claude Code agent not found');
|
||||
|
||||
// Get fresh session state to avoid stale closure
|
||||
const freshSession = sessionsRef.current.find(s => s.id === activeSessionId);
|
||||
if (!freshSession) throw new Error('Session not found');
|
||||
|
||||
const freshActiveTab = getActiveTab(freshSession);
|
||||
const tabClaudeSessionId = freshActiveTab?.claudeSessionId;
|
||||
|
||||
// Build spawn args with resume if we have a session ID
|
||||
const spawnArgs = [...(agent.args || [])];
|
||||
if (tabClaudeSessionId) {
|
||||
spawnArgs.push('--resume', tabClaudeSessionId);
|
||||
}
|
||||
|
||||
// Add read-only mode if tab has it enabled
|
||||
if (freshActiveTab?.readOnlyMode) {
|
||||
spawnArgs.push('--permission-mode', 'plan');
|
||||
}
|
||||
|
||||
const commandToUse = agent.path || agent.command;
|
||||
await window.maestro.process.spawn({
|
||||
sessionId: targetSessionId,
|
||||
toolType: 'claude-code',
|
||||
cwd: freshSession.cwd,
|
||||
command: commandToUse,
|
||||
args: spawnArgs,
|
||||
prompt: substitutedPrompt
|
||||
});
|
||||
} catch (error: any) {
|
||||
console.error('[handleInputKeyDown] Failed to spawn Claude for slash command:', error);
|
||||
setSessions(prev => prev.map(s => {
|
||||
if (s.id !== activeSessionId) return s;
|
||||
const errorEntry: LogEntry = {
|
||||
id: generateId(),
|
||||
timestamp: Date.now(),
|
||||
source: 'system',
|
||||
text: `Error: Failed to run ${selectedCommand.command} - ${error.message}`
|
||||
};
|
||||
const updatedAiTabs = s.aiTabs.map(tab =>
|
||||
tab.id === activeTab.id
|
||||
? { ...tab, state: 'idle' as const, logs: [...tab.logs, errorEntry] }
|
||||
: tab
|
||||
);
|
||||
return {
|
||||
...s,
|
||||
state: 'idle' as SessionState,
|
||||
busySource: undefined,
|
||||
aiTabs: updatedAiTabs
|
||||
};
|
||||
}));
|
||||
}
|
||||
})();
|
||||
} else {
|
||||
// Claude Code slash command (no prompt property) - send raw command text
|
||||
// Claude Code will expand the command itself from .claude/commands/*.md
|
||||
processInput(selectedCommand.command);
|
||||
}
|
||||
inputRef.current?.focus();
|
||||
}
|
||||
} else if (e.key === 'Escape') {
|
||||
e.preventDefault();
|
||||
@@ -6579,7 +6441,7 @@ export default function MaestroConsole() {
|
||||
theme={theme}
|
||||
tabs={activeSession.aiTabs}
|
||||
activeTabId={activeSession.activeTabId}
|
||||
cwd={activeSession.cwd}
|
||||
projectRoot={activeSession.projectRoot}
|
||||
shortcut={TAB_SHORTCUTS.tabSwitcher}
|
||||
onTabSelect={(tabId) => {
|
||||
setSessions(prev => prev.map(s =>
|
||||
|
||||
@@ -154,7 +154,11 @@ export const InputArea = React.memo(function InputArea(props: InputAreaProps) {
|
||||
);
|
||||
|
||||
// Refs for slash command items to enable scroll-into-view
|
||||
// Reset refs array length when filtered commands change to avoid stale refs
|
||||
const slashCommandItemRefs = useRef<(HTMLDivElement | null)[]>([]);
|
||||
if (slashCommandItemRefs.current.length !== filteredSlashCommands.length) {
|
||||
slashCommandItemRefs.current = slashCommandItemRefs.current.slice(0, filteredSlashCommands.length);
|
||||
}
|
||||
|
||||
// Refs for tab completion items to enable scroll-into-view
|
||||
const tabCompletionItemRefs = useRef<(HTMLDivElement | null)[]>([]);
|
||||
@@ -264,10 +268,10 @@ export const InputArea = React.memo(function InputArea(props: InputAreaProps) {
|
||||
{/* Slash Command Autocomplete */}
|
||||
{slashCommandOpen && filteredSlashCommands.length > 0 && (
|
||||
<div
|
||||
className="absolute bottom-full left-0 right-0 mb-2 border rounded-lg shadow-2xl max-h-64 overflow-hidden"
|
||||
className="absolute bottom-full left-0 right-0 mb-2 border rounded-lg shadow-2xl overflow-hidden"
|
||||
style={{ backgroundColor: theme.colors.bgSidebar, borderColor: theme.colors.border }}
|
||||
>
|
||||
<div className="overflow-y-auto max-h-64 scrollbar-thin">
|
||||
<div className="overflow-y-auto max-h-64 scrollbar-thin" style={{ overscrollBehavior: 'contain' }}>
|
||||
{filteredSlashCommands.map((cmd, idx) => (
|
||||
<div
|
||||
key={cmd.command}
|
||||
@@ -280,11 +284,14 @@ export const InputArea = React.memo(function InputArea(props: InputAreaProps) {
|
||||
color: idx === safeSelectedIndex ? theme.colors.bgMain : theme.colors.textMain
|
||||
}}
|
||||
onClick={() => {
|
||||
// Single click just selects the item
|
||||
setSelectedSlashCommandIndex(idx);
|
||||
}}
|
||||
onDoubleClick={() => {
|
||||
// Double click fills in the command text
|
||||
setInputValue(cmd.command);
|
||||
setSlashCommandOpen(false);
|
||||
inputRef.current?.focus();
|
||||
// Execute the command after a brief delay to let state update
|
||||
setTimeout(() => processInput(), 10);
|
||||
}}
|
||||
onMouseEnter={() => setSelectedSlashCommandIndex(idx)}
|
||||
>
|
||||
@@ -555,9 +562,12 @@ export const InputArea = React.memo(function InputArea(props: InputAreaProps) {
|
||||
|
||||
// Show slash command autocomplete when typing /
|
||||
if (value.startsWith('/') && !value.includes(' ')) {
|
||||
// Only reset selection when modal first opens, not on every keystroke
|
||||
if (!slashCommandOpen) {
|
||||
setSelectedSlashCommandIndex(0);
|
||||
}
|
||||
setSlashCommandOpen(true);
|
||||
// Always reset selection to first item when filter changes
|
||||
setSelectedSlashCommandIndex(0);
|
||||
// Clamp selection if filtered list shrinks (handled by safeSelectedIndex in render)
|
||||
} else {
|
||||
setSlashCommandOpen(false);
|
||||
}
|
||||
|
||||
@@ -611,7 +611,7 @@ export function SessionList(props: SessionListProps) {
|
||||
return (
|
||||
<div
|
||||
tabIndex={0}
|
||||
className={`border-r flex flex-col shrink-0 transition-all duration-300 outline-none relative ${activeFocus === 'sidebar' ? 'ring-1 ring-inset z-10' : ''}`}
|
||||
className={`border-r flex flex-col shrink-0 transition-all duration-300 outline-none relative z-20 ${activeFocus === 'sidebar' ? 'ring-1 ring-inset' : ''}`}
|
||||
style={{
|
||||
width: leftSidebarOpen ? `${leftSidebarWidthState}px` : '64px',
|
||||
backgroundColor: theme.colors.bgSidebar,
|
||||
|
||||
@@ -12,6 +12,7 @@ interface NamedSession {
|
||||
projectPath: string;
|
||||
sessionName: string;
|
||||
starred?: boolean;
|
||||
lastActivityAt?: number;
|
||||
}
|
||||
|
||||
/** Union type for items in the list */
|
||||
@@ -23,7 +24,7 @@ interface TabSwitcherModalProps {
|
||||
theme: Theme;
|
||||
tabs: AITab[];
|
||||
activeTabId: string;
|
||||
cwd: string; // Current working directory for syncing tab names
|
||||
projectRoot: string; // The initial project directory (used for Claude session storage)
|
||||
shortcut?: Shortcut;
|
||||
onTabSelect: (tabId: string) => void;
|
||||
onNamedSessionSelect: (claudeSessionId: string, projectPath: string, sessionName: string, starred?: boolean) => void;
|
||||
@@ -49,6 +50,32 @@ function formatCost(cost: number): string {
|
||||
return '$' + cost.toFixed(2);
|
||||
}
|
||||
|
||||
/**
|
||||
* Format a timestamp as relative time (e.g., "5m ago", "2h ago", "Dec 3")
|
||||
*/
|
||||
function formatRelativeTime(timestamp: number): string {
|
||||
const now = Date.now();
|
||||
const diffMs = now - timestamp;
|
||||
const diffMins = Math.floor(diffMs / 60000);
|
||||
const diffHours = Math.floor(diffMins / 60);
|
||||
const diffDays = Math.floor(diffHours / 24);
|
||||
|
||||
if (diffMins < 1) return 'Now';
|
||||
if (diffMins < 60) return `${diffMins}m ago`;
|
||||
if (diffHours < 24) return `${diffHours}h ago`;
|
||||
if (diffDays < 7) return `${diffDays}d ago`;
|
||||
return new Date(timestamp).toLocaleDateString([], { month: 'short', day: 'numeric' });
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the last activity timestamp from a tab's logs
|
||||
*/
|
||||
function getTabLastActivity(tab: AITab): number | undefined {
|
||||
if (!tab.logs || tab.logs.length === 0) return undefined;
|
||||
// Get the most recent log entry timestamp
|
||||
return Math.max(...tab.logs.map(log => log.timestamp));
|
||||
}
|
||||
|
||||
/**
|
||||
* Get context usage percentage from usage stats
|
||||
* Uses inputTokens + outputTokens (not cache tokens) to match MainPanel calculation
|
||||
@@ -141,7 +168,7 @@ export function TabSwitcherModal({
|
||||
theme,
|
||||
tabs,
|
||||
activeTabId,
|
||||
cwd,
|
||||
projectRoot,
|
||||
shortcut,
|
||||
onTabSelect,
|
||||
onNamedSessionSelect,
|
||||
@@ -208,7 +235,7 @@ export function TabSwitcherModal({
|
||||
const namedTabs = tabs.filter(t => t.name && t.claudeSessionId);
|
||||
await Promise.all(
|
||||
namedTabs.map(tab =>
|
||||
window.maestro.claude.updateSessionName(cwd, tab.claudeSessionId!, tab.name!)
|
||||
window.maestro.claude.updateSessionName(projectRoot, tab.claudeSessionId!, tab.name!)
|
||||
.catch(err => console.warn('[TabSwitcher] Failed to sync tab name:', err))
|
||||
)
|
||||
);
|
||||
@@ -221,7 +248,7 @@ export function TabSwitcherModal({
|
||||
if (!namedSessionsLoaded) {
|
||||
syncAndLoad();
|
||||
}
|
||||
}, [namedSessionsLoaded, tabs, cwd]);
|
||||
}, [namedSessionsLoaded, tabs, projectRoot]);
|
||||
|
||||
// Scroll selected item into view
|
||||
useEffect(() => {
|
||||
@@ -254,7 +281,7 @@ export function TabSwitcherModal({
|
||||
});
|
||||
return sorted.map(tab => ({ type: 'open' as const, tab }));
|
||||
} else {
|
||||
// All Named mode - show ALL named sessions (including open ones)
|
||||
// All Named mode - show named sessions for the CURRENT PROJECT only (including open ones)
|
||||
// For open tabs, use the 'open' type so we get usage stats; for closed ones use 'named'
|
||||
const items: ListItem[] = [];
|
||||
|
||||
@@ -265,9 +292,9 @@ export function TabSwitcherModal({
|
||||
}
|
||||
}
|
||||
|
||||
// Add closed named sessions (not currently open)
|
||||
// Add closed named sessions from the SAME PROJECT (not currently open)
|
||||
for (const session of namedSessions) {
|
||||
if (!openTabSessionIds.has(session.claudeSessionId)) {
|
||||
if (session.projectPath === projectRoot && !openTabSessionIds.has(session.claudeSessionId)) {
|
||||
items.push({ type: 'named' as const, session });
|
||||
}
|
||||
}
|
||||
@@ -281,7 +308,7 @@ export function TabSwitcherModal({
|
||||
|
||||
return items;
|
||||
}
|
||||
}, [viewMode, tabs, namedSessions, openTabSessionIds]);
|
||||
}, [viewMode, tabs, namedSessions, openTabSessionIds, projectRoot]);
|
||||
|
||||
// Filter items based on search query
|
||||
const filteredItems = useMemo(() => {
|
||||
@@ -518,6 +545,10 @@ export function TabSwitcherModal({
|
||||
<span>{formatCost(cost)}</span>
|
||||
</>
|
||||
)}
|
||||
{(() => {
|
||||
const lastActivity = getTabLastActivity(tab);
|
||||
return lastActivity ? <span>{formatRelativeTime(lastActivity)}</span> : null;
|
||||
})()}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -578,7 +609,9 @@ export function TabSwitcherModal({
|
||||
)}
|
||||
</div>
|
||||
<div className="flex items-center gap-3 text-[10px] opacity-60">
|
||||
<span className="truncate">{session.projectPath.split('/').slice(-2).join('/')}</span>
|
||||
{session.lastActivityAt && (
|
||||
<span>{formatRelativeTime(session.lastActivityAt)}</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -578,7 +578,7 @@ const LogItemComponent = memo(({
|
||||
) : isAIMode && !markdownRawMode ? (
|
||||
// Collapsed markdown preview with rendered markdown
|
||||
// Note: prose styles are injected once at TerminalOutput container level for performance
|
||||
<div className="prose prose-sm max-w-none" style={{ color: theme.colors.textMain, lineHeight: 1.4 }}>
|
||||
<div className="prose prose-sm max-w-none" style={{ color: theme.colors.textMain, lineHeight: 1.4, paddingLeft: '0.5em' }}>
|
||||
<ReactMarkdown
|
||||
remarkPlugins={[remarkGfm]}
|
||||
components={{
|
||||
@@ -703,7 +703,7 @@ const LogItemComponent = memo(({
|
||||
) : isAIMode && !markdownRawMode ? (
|
||||
// Expanded markdown rendering
|
||||
// Note: prose styles are injected once at TerminalOutput container level for performance
|
||||
<div className="prose prose-sm max-w-none text-sm" style={{ color: theme.colors.textMain, lineHeight: 1.4 }}>
|
||||
<div className="prose prose-sm max-w-none text-sm" style={{ color: theme.colors.textMain, lineHeight: 1.4, paddingLeft: '0.5em' }}>
|
||||
<ReactMarkdown
|
||||
remarkPlugins={[remarkGfm]}
|
||||
components={{
|
||||
@@ -810,7 +810,7 @@ const LogItemComponent = memo(({
|
||||
) : isAIMode && !markdownRawMode ? (
|
||||
// Rendered markdown for AI responses
|
||||
// Note: prose styles are injected once at TerminalOutput container level for performance
|
||||
<div className="prose prose-sm max-w-none text-sm" style={{ color: theme.colors.textMain, lineHeight: 1.4 }}>
|
||||
<div className="prose prose-sm max-w-none text-sm" style={{ color: theme.colors.textMain, lineHeight: 1.4, paddingLeft: '0.5em' }}>
|
||||
<ReactMarkdown
|
||||
remarkPlugins={[remarkGfm]}
|
||||
components={{
|
||||
@@ -1567,8 +1567,8 @@ export const TerminalOutput = forwardRef<HTMLDivElement, TerminalOutputProps>((p
|
||||
.prose p { color: ${theme.colors.textMain}; margin: 0 !important; line-height: 1.4; }
|
||||
.prose p + p { margin-top: 0.5em !important; }
|
||||
.prose p:empty { display: none; }
|
||||
.prose > ul, .prose > ol { color: ${theme.colors.textMain}; margin: 0.25em 0 !important; padding-left: 0.5em; list-style-position: inside; }
|
||||
.prose li ul, .prose li ol { margin: 0 !important; padding-left: 1em; list-style-position: inside; }
|
||||
.prose > ul, .prose > ol { color: ${theme.colors.textMain}; margin: 0.25em 0 !important; padding-left: 2em; list-style-position: outside; }
|
||||
.prose li ul, .prose li ol { margin: 0 !important; padding-left: 1.5em; list-style-position: outside; }
|
||||
.prose li { margin: 0 !important; padding: 0; line-height: 1.4; display: list-item; }
|
||||
.prose li > p { margin: 0 !important; display: contents !important; }
|
||||
.prose li > p + ul, .prose li > p + ol { margin-top: 0 !important; }
|
||||
|
||||
1
src/renderer/global.d.ts
vendored
1
src/renderer/global.d.ts
vendored
@@ -308,6 +308,7 @@ interface MaestroAPI {
|
||||
projectPath: string;
|
||||
sessionName: string;
|
||||
starred?: boolean;
|
||||
lastActivityAt?: number;
|
||||
}>>;
|
||||
deleteMessagePair: (projectPath: string, sessionId: string, userMessageUuid: string, fallbackContent?: string) => Promise<{ success: boolean; linesRemoved?: number; error?: string }>;
|
||||
};
|
||||
|
||||
@@ -261,6 +261,7 @@ export interface Session {
|
||||
state: SessionState;
|
||||
cwd: string;
|
||||
fullPath: string;
|
||||
projectRoot: string; // The initial working directory (never changes, used for Claude session storage)
|
||||
aiLogs: LogEntry[];
|
||||
shellLogs: LogEntry[];
|
||||
workLog: WorkLogItem[];
|
||||
|
||||
Reference in New Issue
Block a user