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
17 changes: 16 additions & 1 deletion apps/mobile/src/lib/composerContext.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -159,10 +159,25 @@ describe("mobile composer context", () => {
it("keeps exact source positions and repeated references alongside existing native tokens", () => {
const reference = formatComposerContextReference(terminal);
const text = `Use $playwright and [app.ts](src/app.ts) with ${reference} then ${reference}`;
const tokens = composerContextEditorTokens(text, collectComposerInlineTokens(text));
const tokens = composerContextEditorTokens(
text,
collectComposerInlineTokens(text),
new Set(["playwright"]),
);
expect(tokens.map((token) => token.type)).toEqual(["skill", "mention", "context", "context"]);
for (const token of tokens) expect(text.slice(token.start, token.end)).toBe(token.source);
});
it("leaves a $name that is not one of the provider's skills as plain text", () => {
const text = "Run $playwright then echo $HOME and $notaskill ";
const tokensFor = (skills: ReadonlyArray<string>) =>
composerContextEditorTokens(text, collectComposerInlineTokens(text), new Set(skills)).map(
(token) => token.source,
);
expect(tokensFor(["playwright"])).toEqual(["$playwright"]);
// The skill list loads after the editor opens, and differs per provider.
expect(tokensFor([])).toEqual([]);
expect(tokensFor(["playwright", "notaskill"])).toEqual(["$playwright", "$notaskill"]);
});

it("removes deleted payloads but keeps the screenshot linked to a remaining annotation", () => {
const context: OrchestrationMessageContext = {
Expand Down
11 changes: 9 additions & 2 deletions apps/mobile/src/lib/composerContext.ts
Original file line number Diff line number Diff line change
Expand Up @@ -144,11 +144,18 @@ export function pullRequestComposerContext(
}

/** Native editors collapse the canonical source range to a single atomic attachment. */
export function composerContextEditorTokens(text: string, tokens: readonly ComposerInlineToken[]) {
export function composerContextEditorTokens(
text: string,
tokens: readonly ComposerInlineToken[],
knownSkills: { has(name: string): boolean },
) {
const references = collectComposerContextReferences(text);
return [
...tokens.filter(
(token) => !references.some((ref) => token.start < ref.end && token.end > ref.start),
(token) =>
// `$HOME` and other `$name` text that is not one of the provider's skills stays prose.
(token.type !== "skill" || knownSkills.has(token.value)) &&
!references.some((ref) => token.start < ref.end && token.end > ref.start),
),
...references.map((ref) => ({
type: "context" as const,
Expand Down
2 changes: 1 addition & 1 deletion apps/mobile/src/native/T3ComposerEditor.ios.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -168,7 +168,7 @@ export function ComposerEditor({
});
confirmedTokensRef.current = tokens;
return JSON.stringify(
composerContextEditorTokens(props.value, tokens).map((token) => {
composerContextEditorTokens(props.value, tokens, skillLabels).map((token) => {
const record =
token.type === "context"
? props.context?.records.find((record) => record.contextId === token.contextId)
Expand Down
2 changes: 1 addition & 1 deletion apps/mobile/src/native/T3ComposerEditor.native.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -166,7 +166,7 @@ export function ComposerEditor({
});
confirmedTokensRef.current = tokens;
return JSON.stringify(
composerContextEditorTokens(props.value, tokens).map((token) => {
composerContextEditorTokens(props.value, tokens, skillLabels).map((token) => {
const record =
token.type === "context"
? props.context?.records.find((record) => record.contextId === token.contextId)
Expand Down
Loading
Loading