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
1 change: 1 addition & 0 deletions packages/plugin-sdk/src/composer-view.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ import type { UiSubscription } from './workbench.js';

export interface ComposerFocusHandle {
focus(): void;
bindKey(key: string, action: () => void, options?: { fallback?: boolean }): () => void;
}

export interface ComposerCommand {
Expand Down
90 changes: 37 additions & 53 deletions plugins/agent/composer/ui/Composer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ import { ProjectMcp } from '../../../../products/maka/workbench/app/ProjectMcpCo
import { tr } from '../../../../products/maka/workbench/app/locale';
import { cleanup, effect, inputAction, signal, Text, View, Image } from 'nsh';
import { ContextMenu, Input, MenuItem, Popover } from 'nsh/base';
import type { InputRef, JSX, ViewRef } from 'nsh';
import type { JSX, ViewRef } from 'nsh';
import { Button } from 'nsh/base';
import { platform, windowSize } from 'nsh';
import { keymap } from 'nsh';
Expand Down Expand Up @@ -55,29 +55,18 @@ import { FileDropHint } from '../../../../products/maka/workbench/app/FileDropHi
import { matchComposerCommand } from '../../../../products/maka/workbench/app/composerCommands';
import { ComposerCompletion, createComposerCompletion } from './ComposerCompletion';
import { createComposerPaste } from './composerPaste';
import { SkillInput } from '../../../../products/maka/workbench/app/SkillInput';
import type { ComposerEditor } from '../../../../products/maka/workbench/app/ComposerEditor';
import logo from '../../../../products/maka/workbench/app/assets/maka.png';

// Match the skill invocation grammar; the surrounding whitespace is not part of the chip.
const skillTokens = () => [
{
pattern: String.raw`(?:^|[\t-\r \u{00a0}\u{1680}\u{2000}-\u{200a}\u{2028}\u{2029}\u{202f}\u{205f}\u{3000}\u{feff}])(?P<token>/skill:[A-Za-z0-9._-]+)`,
style: {
color: C.accent,
backgroundColor: `${C.accent}20`,
borderRadius: 4,
},
atomic: true,
},
];

export function Composer(props: ComposerViewInput<JSX.Element>) {
const pendingAdd = signal(false);
const completion = createComposerCompletion({
session: props.session,
commands: props.commands,
active: props.active,
});
const predictionEditor = signal<{ owner: ComposerSession; node: InputRef } | null>(null);
const predictionEditor = signal<{ owner: ComposerSession; node: ComposerEditor } | null>(null);
const suggestedMessage = () => {
const owner = props.session();
if (
Expand Down Expand Up @@ -136,10 +125,7 @@ export function Composer(props: ComposerViewInput<JSX.Element>) {
text: suggestion.text,
};
const bindings = ['tab', 'right', 'enter'].map((key) =>
keymap.bind(key, () => acceptSuggestedMessage(offered), {
when: 'always',
within: editor.node,
}),
editor.node.bindKey(key, () => acceptSuggestedMessage(offered)),
);
return () => bindings.forEach((unbind) => unbind());
});
Expand Down Expand Up @@ -279,6 +265,12 @@ export function Composer(props: ComposerViewInput<JSX.Element>) {
let unbind = () => {};
cleanup(() => unbind());
const cardRoot = signal<ViewRef | null>(null);
const toggleAttachments = () => {
const owner = props.session();
if (owner && owner.editable())
owner.attachments.open({ owner: owner.sessionId, value: !owner.attachments.picker() });
else if (pending()) pending()!.openPicker(!pending()!.picking());
};
// A file drag over the editor leaves the conversation's own drop target.
const draggingOverEditor = signal(false);
const fileDragging = () =>
Expand All @@ -290,25 +282,33 @@ export function Composer(props: ComposerViewInput<JSX.Element>) {
const node = cardRoot();
if (node && props.active() && owner?.editable() && owner.invocation() === null && owner.draft())
return keymap.bind('ctrl+c', completion.clearDraft, {
when: 'always',
when: 'notEditing',
within: node,
fallback: true,
});
});
effect(() => {
const stop = props.stopAction();
const owner = props.session();
const current = predictionEditor();
if (
owner &&
current?.owner === owner &&
props.active() &&
owner.editable() &&
owner.invocation() === null &&
owner.draft()
)
return current.node.bindKey('ctrl+c', completion.clearDraft, { fallback: true });
});
const escapeComposer = () => {
if (completion.composing()) return;
const suggestion = suggestedMessage();
if (suggestion) props.session()!.prediction.dismiss(suggestion.sourceTurnId);
else props.stopAction()();
};
effect(() => {
const node = cardRoot();
if (node)
return keymap.bind(
'escape',
() => {
if (completion.composing()) return;
const suggestion = suggestedMessage();
if (suggestion) props.session()!.prediction.dismiss(suggestion.sourceTurnId);
else stop();
},
{ when: 'always', within: node },
);
if (node) return keymap.bind('escape', escapeComposer, { when: 'notEditing', within: node });
});
const workspaceLabel = () => {
const path = props.session()?.workspace() ?? pending()?.workspace();
Expand Down Expand Up @@ -570,13 +570,7 @@ export function Composer(props: ComposerViewInput<JSX.Element>) {
unbind = keymap.bind(
'mod+u',
() => {
const owner = props.session();
if (owner && owner.editable())
owner.attachments.open({
owner: owner.sessionId,
value: !owner.attachments.picker(),
});
else if (pending()) pending()!.openPicker(!pending()!.picking());
toggleAttachments();
},
{ when: 'always', within: node },
);
Expand Down Expand Up @@ -607,7 +601,7 @@ export function Composer(props: ComposerViewInput<JSX.Element>) {
const owner = session();
const local = owner === null ? pending() : null;
const path = signal('');
const editor = signal<InputRef | null>(null);
const editor = signal<ComposerEditor | null>(null);
const selection = signal({ start: 0, end: 0 });
const paste = local
? createComposerPaste({
Expand Down Expand Up @@ -719,19 +713,18 @@ export function Composer(props: ComposerViewInput<JSX.Element>) {
maxHeight: compact() ? 64 : 144,
}}
>
<Input
<SkillInput
ref={(node) => {
editor.set(node);
props.attach(node);
node.bindKey('mod+u', toggleAttachments);
completion.attach(owner?.sessionId ?? null, node);
predictionEditor.set(owner ? { owner, node } : null);
}}
onEscape={escapeComposer}
testID="message"
accessibilityLabel={tr('Message composer')}
autoFocus={owner !== null || local !== null}
multiline
submitBehavior="submit"
tokens={skillTokens()}
value={owner?.draft() ?? local?.draft() ?? ''}
readonly={owner ? !owner.editable() : !local || local.sending()}
onChange={
Expand Down Expand Up @@ -791,14 +784,6 @@ export function Composer(props: ComposerViewInput<JSX.Element>) {
: undefined
}
onCaretMove={owner ? (event) => completion.caret(owner, event) : undefined}
onCompositionChange={
owner || local
? (event) => {
if (owner || pending() === local)
completion.composition(owner, event);
}
: undefined
}
onSubmit={
owner
? (event) => {
Expand Down Expand Up @@ -828,7 +813,6 @@ export function Composer(props: ComposerViewInput<JSX.Element>) {
flexGrow: 1,
flexShrink: 1,
color: C.text,
placeholderColor: C.muted,
...bodyTextStyle,
...(owner !== null && suggestedMessage() !== null
? {
Expand Down
60 changes: 24 additions & 36 deletions plugins/agent/composer/ui/ComposerCompletion.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -43,13 +43,17 @@ import type { CompletionToken, Selection } from './composerCompletionSyntax.ts';
import { C, bodyTextStyle, captionTextStyle } from '../../../../products/maka/workbench/app/ui';
import { Icon } from '../../../../products/maka/workbench/app/Icon';
import { createComposerPaste } from './composerPaste';
import {
inputEditor,
type ComposerEditor,
} from '../../../../products/maka/workbench/app/ComposerEditor';

type Source = 'add' | 'files' | 'skills' | 'commands' | 'slash';
type Editor = { sessionId: string | null; node: InputRef };
type Editor = { sessionId: string | null; node: ComposerEditor };
type Read = {
serial: number;
owner: ComposerSession;
editor: InputRef;
editor: ComposerEditor;
text: string;
selection: Selection;
token: CompletionToken | null;
Expand All @@ -65,7 +69,7 @@ type Row = {
id: string;
title: string;
detail?: string;
icon: 'paperclip' | 'file' | 'folder' | 'code' | 'command';
icon: 'paperclip' | 'file' | 'folder' | 'book' | 'code' | 'command';
disabled?: boolean;
action: { read: number; id: string };
} & (
Expand Down Expand Up @@ -97,10 +101,10 @@ export function createComposerCompletion(options: Options) {
const editor = signal<Editor | null>(null);
const caret = signal<Bounds | null>(null);
const selection = signal<Selection>({ start: 0, end: 0 });
const composing = signal(false);
const composing = () => !!editor()?.node.composing();
const explicit = signal<Source | null>(null);
const query = signal('');
const queryInput = signal<InputRef | null>(null);
const queryInput = signal<ComposerEditor | null>(null);
const querySelection = signal<Selection>({ start: 0, end: 0 });
const queryComposing = signal(false);
const opened = signal<Read | null>(null);
Expand Down Expand Up @@ -248,7 +252,7 @@ export function createComposerCompletion(options: Options) {
return tr('Use a skill');
},
detail: '/',
icon: 'code',
icon: 'book',
}),
...options
.commands()
Expand Down Expand Up @@ -370,7 +374,7 @@ export function createComposerCompletion(options: Options) {
insert: skill.invocation,
title: skill.name,
detail: skill.description,
icon: 'code',
icon: 'book',
}),
),
);
Expand Down Expand Up @@ -635,47 +639,35 @@ export function createComposerCompletion(options: Options) {
if (!target) return;
const valid = () => current === read && owns(read);
const unbind = [
keymap.bind(
'escape',
() => {
if (valid()) dismiss();
},
{ when: 'always', within: target },
),
target.bindKey('escape', () => {
if (valid()) dismiss();
}),
];
if (loading() || rows().some((item) => !item.disabled)) {
unbind.push(
keymap.bind(
target.bindKey(
'down',
() => {
if (valid()) move(1);
},
{
when: 'always',
repeat: true,
within: target,
},
),
keymap.bind(
target.bindKey(
'up',
() => {
if (valid()) move(-1);
},
{
when: 'always',
repeat: true,
within: target,
},
),
keymap.bind(
'enter',
() => {
if (!valid()) return;
const item = rows()[index()];
if (item) accept(item.action);
},
{ when: 'always', within: target },
),
target.bindKey('enter', () => {
if (!valid()) return;
const item = rows()[index()];
if (item) accept(item.action);
}),
);
}
return () => unbind.forEach((remove) => remove());
Expand Down Expand Up @@ -724,16 +716,15 @@ export function createComposerCompletion(options: Options) {
});
close();
},
anchor: () => (opened()?.explicit ? addAnchor : (caret() ?? editor()?.node)),
anchor: () => (opened()?.explicit ? addAnchor : (caret() ?? editor()?.node.anchor)),
addButton(node: ViewRef) {
addAnchor = node;
},
attach(sessionId: string | null, node: InputRef) {
attach(sessionId: string | null, node: ComposerEditor) {
directoryHint = null;
editor.set({ sessionId, node });
selection.set({ start: 0, end: 0 });
caret.set(null);
composing.set(false);
queryComposing.set(false);
explicit.set(null);
},
Expand All @@ -748,9 +739,6 @@ export function createComposerCompletion(options: Options) {
selection(owner: ComposerSession, event: Selection) {
if (options.session() === owner) selection.set({ ...event });
},
composition(owner: ComposerSession | null, event: { composing: boolean }) {
if (options.session() === owner) composing.set(event.composing);
},
toggleAdd(target: { sessionId: string | null }) {
const owner = options.session();
if (
Expand Down Expand Up @@ -788,7 +776,7 @@ export function createComposerCompletion(options: Options) {
list = node;
},
queryInput(node: InputRef) {
queryInput.set(node);
queryInput.set(inputEditor(node, queryComposing));
querySelection.set({ start: 0, end: 0 });
node.focus();
},
Expand Down
Loading
Loading