feat(chat): align command, shell, and subtask UX (#444)

* feat: reload interface after skills operations

- Adds configurable delay before interface reload after skills changes
- Introduces polling to wait for application health after reload
- Updates UI to show reload message when installing or modifying skills

* feat: distinguish skills from commands in UI

- Displays skill badge for commands that are registered skills
- Prevents editing skills through command management interface
- Triggers interface reload after skill operations to reflect changes

* feat(chat): align command and subtask UX with opencode parity

Route commands/shell via parity paths, render delegated subtasks cleanly, and surface child-session permission/question prompts in parent chat.

* fix(ProjectEditDialog): improve layout consistency

* fix: update task icon and session handling in ToolPart

* feat(chat): add shell-mode input and collapse shell bridge output

Switch leading ! to shell mode UX and fold synthetic shell bridge assistant messages into the user shell bubble with inline output actions.

* fix: remove AI agent icon from file mention autocomplete

* fix: remove unused icon import from file mention component
This commit is contained in:
Bohdan Triapitsyn
2026-02-18 20:08:42 +02:00
committed by GitHub
parent e4a2486312
commit 85f21cb945
23 changed files with 1557 additions and 318 deletions
+43 -3
View File
@@ -793,6 +793,28 @@ export const useEventStream = () => {
break;
}
const shouldKeepSyntheticUserText = (value: unknown): boolean => {
const text = typeof value === 'string' ? value.trim() : '';
if (!text) return false;
return (
text.startsWith('User has requested to enter plan mode') ||
text.startsWith('The plan at ') ||
text.startsWith('The following tool was executed by the user')
);
};
const inferUserRoleFromPart = (): boolean => {
const partType = typeof partExt.type === 'string' ? partExt.type : '';
if (partType === 'subtask' || partType === 'agent' || partType === 'file') {
return true;
}
if (partType === 'text' && partExt.synthetic === true) {
const text = (partExt as { text?: unknown }).text;
return shouldKeepSyntheticUserText(text);
}
return false;
};
let roleInfo = 'assistant';
if (messageInfo && typeof (messageInfo as { role?: unknown }).role === 'string') {
roleInfo = (messageInfo as { role?: string }).role as string;
@@ -806,11 +828,18 @@ export const useEventStream = () => {
}
}
if (roleInfo !== 'user' && inferUserRoleFromPart()) {
roleInfo = 'user';
}
trackMessage(messageId, 'part_received', { role: roleInfo });
if (roleInfo === 'user' && partExt.synthetic === true) {
trackMessage(messageId, 'skipped_synthetic_user_part');
break;
const text = (partExt as { text?: unknown }).text;
if (!shouldKeepSyntheticUserText(text)) {
trackMessage(messageId, 'skipped_synthetic_user_part');
break;
}
}
const messagePart: Part = {
@@ -1162,7 +1191,8 @@ export const useEventStream = () => {
const textStr = typeof text === 'string' ? text.trim() : '';
const shouldKeep =
textStr.startsWith('User has requested to enter plan mode') ||
textStr.startsWith('The plan at ');
textStr.startsWith('The plan at ') ||
textStr.startsWith('The following tool was executed by the user');
if (!shouldKeep) continue;
}
@@ -1457,6 +1487,11 @@ export const useEventStream = () => {
return;
}
const requestSession = useSessionStore.getState().sessions.find((session) => session.id === request.sessionID);
if (requestSession?.parentID && requestSession.parentID === current) {
return;
}
const pending = useSessionStore
.getState()
.permissions
@@ -1514,6 +1549,11 @@ export const useEventStream = () => {
return;
}
const requestSession = useSessionStore.getState().sessions.find((session) => session.id === request.sessionID);
if (requestSession?.parentID && requestSession.parentID === current) {
return;
}
const pending = useSessionStore
.getState()
.questions