Skip to content
Open
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
4 changes: 4 additions & 0 deletions src/hooks/useAgent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,8 @@ export interface UseAgentReturn {
setMessagesFromLocal: (localMessages: ChatMessage[]) => void;
clearError: () => void;
setIgnoreUpdates: (ignore: boolean) => void;
/** Ask the agent for starter prompts; returns parsed list. */
generateStarterPrompts: () => Promise<string[]>;
// Permission
activePermission: ActivePermission | null;
hasActivePermission: boolean;
Expand Down Expand Up @@ -209,6 +211,7 @@ export function useAgent(
setMessagesFromLocal: agentMessages.setMessagesFromLocal,
clearError: agentMessages.clearError,
setIgnoreUpdates: agentMessages.setIgnoreUpdates,
generateStarterPrompts: agentMessages.generateStarterPrompts,

// Permission
activePermission: agentMessages.activePermission,
Expand Down Expand Up @@ -240,6 +243,7 @@ export function useAgent(
agentMessages.setMessagesFromLocal,
agentMessages.clearError,
agentMessages.setIgnoreUpdates,
agentMessages.generateStarterPrompts,
agentMessages.activePermission,
agentMessages.hasActivePermission,
agentMessages.approvePermission,
Expand Down
104 changes: 102 additions & 2 deletions src/hooks/useAgentMessages.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,49 @@ import {
selectOption,
} from "../services/message-state";

// ============================================================================
// Starter Prompt Suggestions
// ============================================================================

/** Maximum number of starter prompts to surface as chips. */
const MAX_SUGGESTIONS = 4;

/**
* Instruction sent once when a fresh chat opens. The reply is parsed into
* starter prompt chips and suppressed from the visible message list.
*/
const STARTER_PROMPT_INSTRUCTION = `Suggest up to ${MAX_SUGGESTIONS} concise prompts I could use to get started with you in this workspace. Respond with ONLY the prompts, one per line - no numbering, no bullets, no commentary, no surrounding quotes. Keep each prompt under 12 words.`;

function parseStarterPrompts(text: string): string[] {
const out: string[] = [];
const seen = new Set<string>();
const instructionNeedles = [
"suggest up to",
"concise prompts",
"respond with only",
"no numbering",
"no bullets",
"keep each prompt",
];
for (const rawLine of text.split(/\r?\n/)) {
let line = rawLine.trim();
if (!line) continue;
line = line.replace(/^(?:[-*]\s+|\d+[.)]\s+)/, "").trim();
line = line.replace(/^["'`]+|["'`]+$/g, "").trim();
if (!line) continue;
const key = line.toLowerCase();
if (instructionNeedles.some((needle) => key.includes(needle))) {
continue;
}
if (line.length > 200) continue;
if (seen.has(key)) continue;
seen.add(key);
out.push(line);
if (out.length >= MAX_SUGGESTIONS) break;
}
return out;
}

// ============================================================================
// Types
// ============================================================================
Expand Down Expand Up @@ -87,6 +130,8 @@ export interface UseAgentMessagesReturn {

/** Enqueue a message-level update (used by useAgent for unified handler) */
enqueueUpdate: (update: SessionUpdate) => void;
/** Ask the agent for starter prompts; returns parsed list. */
generateStarterPrompts: () => Promise<string[]>;
}

// ============================================================================
Expand All @@ -108,7 +153,7 @@ export function useAgentMessages(
const [isSending, setIsSending] = useState(false);
const [lastUserMessage, setLastUserMessage] = useState<string | null>(null);

// Tool call index: toolCallId message index for O(1) lookup
// Tool call index: toolCallId to message index for O(1) lookup
const toolCallIndexRef = useRef<Map<string, number>>(new Map());

// Ignore updates flag (used during session/load to skip history replay)
Expand All @@ -123,6 +168,7 @@ export function useAgentMessages(
// Track the current send promise so a new sendMessage() can wait for
// the previous one to settle before starting (avoids interleaved sends).
const sendPromiseRef = useRef<Promise<void> | null>(null);
const isGeneratingStarterPromptsRef = useRef(false);

// ============================================================
// Streaming Update Batching
Expand Down Expand Up @@ -256,7 +302,11 @@ export function useAgentMessages(
// Wait for any in-flight send to settle (e.g. after cancel/stop)
// before starting a new one to avoid interleaved state updates.
if (sendPromiseRef.current) {
try { await sendPromiseRef.current; } catch { /* ignore */ }
try {
await sendPromiseRef.current;
} catch {
/* ignore */
}
}

const currentSessionId = session.sessionId;
Expand Down Expand Up @@ -403,6 +453,55 @@ export function useAgentMessages(
],
);

// ============================================================
// Starter Prompt Suggestions
// ============================================================

const generateStarterPrompts = useCallback(async (): Promise<string[]> => {
const sessionId = session.sessionId;
if (!sessionId) return [];
if (isGeneratingStarterPromptsRef.current) return [];

if (sendPromiseRef.current) {
try {
await sendPromiseRef.current;
} catch {
/* ignore */
}
}

isGeneratingStarterPromptsRef.current = true;

let captured = "";
const unsubscribe = agentClient.onSessionUpdate((update) => {
if (
update.type === "agent_message_chunk" &&
update.sessionId === sessionId
) {
captured += update.text;
}
});
setIgnoreUpdates(true);

const genPromise = agentClient.sendPrompt(sessionId, [
{ type: "text", text: STARTER_PROMPT_INSTRUCTION },
]);
sendPromiseRef.current = genPromise;

try {
await genPromise;
} catch {
captured = "";
} finally {
sendPromiseRef.current = null;
unsubscribe();
setIgnoreUpdates(false);
isGeneratingStarterPromptsRef.current = false;
}

return parseStarterPrompts(captured);
}, [agentClient, session.sessionId, setIgnoreUpdates]);

// ============================================================
// Permission State & Operations
// ============================================================
Expand Down Expand Up @@ -476,5 +575,6 @@ export function useAgentMessages(
approveActivePermission,
rejectActivePermission,
enqueueUpdate,
generateStarterPrompts,
};
}
3 changes: 3 additions & 0 deletions src/hooks/useChatActions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,7 @@
messages: ChatMessage[],
settings: AgentClientPluginSettings,
vaultPath: string,
persistentSourcePath?: string,
): UseChatActionsReturn {
const logger = getLogger();

Expand Down Expand Up @@ -126,7 +127,7 @@
logger.log(`Chat auto-exported before ${context}`);
}
} catch {
new Notice("[Agent Client] Failed to export chat");

Check failure on line 130 in src/hooks/useChatActions.ts

View workflow job for this annotation

GitHub Actions / lint-and-build

Use sentence case for UI text
}
},
[plugin, logger],
Expand Down Expand Up @@ -193,6 +194,7 @@
await sessionHistory.saveSessionLocally(
session.sessionId,
content,
persistentSourcePath,
);
logger.log(
`[ChatPanel] Session saved locally: ${session.sessionId}`,
Expand All @@ -205,6 +207,7 @@
messages.length,
session.sessionId,
sessionHistory.saveSessionLocally,
persistentSourcePath,
logger,
suggestions.mentions.activeNote,
suggestions.mentions.isAutoMentionDisabled,
Expand All @@ -229,7 +232,7 @@

// Skip if already empty AND not switching agents
if (messages.length === 0 && !isAgentSwitch) {
new Notice("[Agent Client] Already a new session");

Check failure on line 235 in src/hooks/useChatActions.ts

View workflow job for this annotation

GitHub Actions / lint-and-build

Use sentence case for UI text
return;
}

Expand Down Expand Up @@ -274,7 +277,7 @@

const handleExportChat = useCallback(async () => {
if (messages.length === 0) {
new Notice("[Agent Client] No messages to export");

Check failure on line 280 in src/hooks/useChatActions.ts

View workflow job for this annotation

GitHub Actions / lint-and-build

Use sentence case for UI text
return;
}

Expand All @@ -291,7 +294,7 @@
);
new Notice(`[Agent Client] Chat exported to ${filePath}`);
} catch (error) {
new Notice("[Agent Client] Failed to export chat");

Check failure on line 297 in src/hooks/useChatActions.ts

View workflow job for this annotation

GitHub Actions / lint-and-build

Use sentence case for UI text
logger.error("Export error:", error);
}
}, [messages, session, plugin, logger]);
Expand All @@ -318,9 +321,9 @@

try {
await agent.forceRestartAgent();
new Notice("[Agent Client] Agent restarted");

Check failure on line 324 in src/hooks/useChatActions.ts

View workflow job for this annotation

GitHub Actions / lint-and-build

Use sentence case for UI text
} catch (error) {
new Notice("[Agent Client] Failed to restart agent");

Check failure on line 326 in src/hooks/useChatActions.ts

View workflow job for this annotation

GitHub Actions / lint-and-build

Use sentence case for UI text
logger.error("Restart error:", error);
}
}, [
Expand Down
8 changes: 7 additions & 1 deletion src/hooks/useSessionHistory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -188,6 +188,7 @@ export interface UseSessionHistoryReturn {
saveSessionLocally: (
sessionId: string,
messageContent: string,
sourcePath?: string,
) => Promise<void>;

/**
Expand Down Expand Up @@ -779,7 +780,11 @@ export function useSessionHistory(
* Called when the first message is sent in a new session.
*/
const saveSessionLocally = useCallback(
async (sessionId: string, messageContent: string) => {
async (
sessionId: string,
messageContent: string,
sourcePath?: string,
) => {
if (!session.agentId) return;

const title = truncateTitle(messageContent);
Expand All @@ -789,6 +794,7 @@ export function useSessionHistory(
agentId: session.agentId,
cwd: agentCwd,
title,
sourcePath,
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
});
Expand Down
11 changes: 9 additions & 2 deletions src/hooks/useSuggestions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,7 @@ export function useSuggestions(
plugin: AgentClientPlugin,
availableCommands: SlashCommand[],
autoMentionDefault: boolean,
pinnedActiveNote?: NoteMetadata | null,
): UseSuggestionsReturn {
// ============================================================
// Mention State
Expand All @@ -103,7 +104,9 @@ export function useSuggestions(
const [mentionContext, setMentionContext] = useState<MentionContext | null>(
null,
);
const [activeNote, setActiveNote] = useState<NoteMetadata | null>(null);
const [activeNote, setActiveNote] = useState<NoteMetadata | null>(
pinnedActiveNote ?? null,
);
const [isAutoMentionDisabled, setIsAutoMentionDisabled] = useState(
!autoMentionDefault,
);
Expand Down Expand Up @@ -206,9 +209,13 @@ export function useSuggestions(
}, []);

const updateActiveNote = useCallback(async () => {
if (pinnedActiveNote) {
setActiveNote(pinnedActiveNote);
return;
}
const note = await vaultAccess.getActiveNote();
setActiveNote(note);
}, [vaultAccess]);
}, [vaultAccess, pinnedActiveNote]);

// ============================================================
// Command Callbacks
Expand Down
Loading
Loading