Skip to content

Commit 0f2ab98

Browse files
authored
Merge pull request #96 from plotday/feature/twist-options
Twist options
2 parents 59cc5d0 + f573ef1 commit 0f2ab98

6 files changed

Lines changed: 152 additions & 6 deletions

File tree

.changeset/khaki-plums-mate.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@plotday/twister": minor
3+
---
4+
5+
Added: Options tool for defining user-configurable options for twists

twister/package.json

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,11 @@
3535
"types": "./dist/tag.d.ts",
3636
"default": "./dist/tag.js"
3737
},
38+
"./options": {
39+
"@plotday/source": "./src/options.ts",
40+
"types": "./dist/options.d.ts",
41+
"default": "./dist/options.js"
42+
},
3843
"./tools/twists": {
3944
"@plotday/source": "./src/tools/twists.ts",
4045
"types": "./dist/tools/twists.d.ts",

twister/src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,5 +3,6 @@ export * from "./plot";
33
export * from "./tag";
44
export * from "./tool";
55
export * from "./tools";
6+
export * from "./options";
67
export * from "./utils/types";
78
export { getBuilderDocumentation } from "./creator-docs";

twister/src/options.ts

Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
import { ITool } from "./tool";
2+
3+
/**
4+
* A select option definition for twist configuration.
5+
* Renders as a dropdown in the Flutter UI.
6+
*/
7+
export type SelectDef = {
8+
type: "select";
9+
label: string;
10+
description?: string;
11+
choices: ReadonlyArray<{ value: string; label: string }>;
12+
default: string;
13+
};
14+
15+
/**
16+
* A text input option definition for twist configuration.
17+
* Renders as a text field in the Flutter UI.
18+
*/
19+
export type TextDef = {
20+
type: "text";
21+
label: string;
22+
description?: string;
23+
default: string;
24+
placeholder?: string;
25+
};
26+
27+
/**
28+
* A number input option definition for twist configuration.
29+
* Renders as a number field in the Flutter UI.
30+
*/
31+
export type NumberDef = {
32+
type: "number";
33+
label: string;
34+
description?: string;
35+
default: number;
36+
min?: number;
37+
max?: number;
38+
};
39+
40+
/**
41+
* A boolean toggle option definition for twist configuration.
42+
* Renders as a switch in the Flutter UI.
43+
*/
44+
export type BooleanDef = {
45+
type: "boolean";
46+
label: string;
47+
description?: string;
48+
default: boolean;
49+
};
50+
51+
/**
52+
* Union of all option definition types.
53+
*/
54+
export type OptionDef = SelectDef | TextDef | NumberDef | BooleanDef;
55+
56+
/**
57+
* Schema defining all configurable options for a twist.
58+
* Each key maps to an option definition that describes its type, label, and default value.
59+
*/
60+
export type OptionsSchema = Record<string, OptionDef>;
61+
62+
/**
63+
* Infers the resolved value types from an options schema.
64+
* Boolean options resolve to `boolean`, number options to `number`,
65+
* and select/text options to `string`.
66+
*/
67+
export type ResolvedOptions<T extends OptionsSchema> = {
68+
[K in keyof T]: T[K] extends BooleanDef
69+
? boolean
70+
: T[K] extends NumberDef
71+
? number
72+
: string;
73+
};
74+
75+
/**
76+
* Built-in marker class for twist configuration options.
77+
*
78+
* Declare options in your twist's `build()` method to expose configurable
79+
* settings to users. The schema is introspected at deploy time and stored
80+
* alongside permissions. At runtime, user values are merged with defaults.
81+
*
82+
* @example
83+
* ```typescript
84+
* import { Options, type OptionsSchema } from "@plotday/twister/options";
85+
*
86+
* export default class MyTwist extends Twist<MyTwist> {
87+
* build(build: ToolBuilder) {
88+
* return {
89+
* options: build(Options, {
90+
* model: {
91+
* type: 'select',
92+
* label: 'AI Model',
93+
* choices: [
94+
* { value: 'fast', label: 'Fast' },
95+
* { value: 'smart', label: 'Smart' },
96+
* ],
97+
* default: 'fast',
98+
* },
99+
* }),
100+
* // ... other tools
101+
* };
102+
* }
103+
*
104+
* async respond(note: Note) {
105+
* const model = this.tools.options.model; // typed as string
106+
* }
107+
* }
108+
* ```
109+
*/
110+
export abstract class Options extends ITool {
111+
static readonly toolId = "Options";
112+
}

twister/src/utils/types.ts

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
*
99
* @internal
1010
*/
11+
import type { Options, OptionsSchema, ResolvedOptions } from "../options";
1112
import type { Callbacks } from "../tools/callbacks";
1213
import type { Store } from "../tools/store";
1314
import type { Tasks } from "../tools/tasks";
@@ -63,10 +64,16 @@ export type InferOptions<T> = T extends {
6364
* Function type for building tool dependencies.
6465
* Used in build methods to request tool instances.
6566
*/
66-
export type ToolBuilder = <TC extends abstract new (...args: any) => any>(
67-
ToolClass: TC,
68-
options?: InferOptions<TC>
69-
) => Promise<InstanceType<TC>>;
67+
export type ToolBuilder = {
68+
<T extends OptionsSchema>(
69+
ToolClass: typeof Options,
70+
schema: T
71+
): Promise<ResolvedOptions<T>>;
72+
<TC extends abstract new (...args: any) => any>(
73+
ToolClass: TC,
74+
options?: InferOptions<TC>
75+
): Promise<InstanceType<TC>>;
76+
};
7077

7178
/**
7279
* Interface for managing tool initialization and lifecycle.

twists/chat/src/index.ts

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,12 +9,28 @@ import {
99
type ToolBuilder,
1010
Twist,
1111
} from "@plotday/twister";
12-
import { AI, type AIMessage } from "@plotday/twister/tools/ai";
12+
import { Options } from "@plotday/twister/options";
13+
import { AI, type AIMessage, AIModel } from "@plotday/twister/tools/ai";
1314
import { ActivityAccess, Plot } from "@plotday/twister/tools/plot";
1415

1516
export default class ChatTwist extends Twist<ChatTwist> {
1617
build(build: ToolBuilder) {
1718
return {
19+
options: build(Options, {
20+
model: {
21+
type: "select" as const,
22+
label: "AI Model",
23+
description: "The AI model used for chat responses",
24+
choices: [
25+
{ value: "anthropic/claude-sonnet-4-5", label: "Claude Sonnet 4.5" },
26+
{ value: "anthropic/claude-haiku-4-5", label: "Claude Haiku 4.5 (Fast)" },
27+
{ value: "openai/gpt-5", label: "GPT-5" },
28+
{ value: "google/gemini-2.5-pro", label: "Gemini 2.5 Pro" },
29+
{ value: "google/gemini-2.5-flash", label: "Gemini 2.5 Flash (Fast)" },
30+
],
31+
default: "anthropic/claude-sonnet-4-5",
32+
},
33+
}),
1834
ai: build(AI),
1935
plot: build(Plot, {
2036
activity: {
@@ -111,7 +127,7 @@ You can provide either or both inline and standalone links. Only use standalone
111127
});
112128

113129
const response = await this.tools.ai.prompt({
114-
model: { speed: "balanced", cost: "medium" },
130+
model: { speed: "balanced", cost: "medium", hint: this.tools.options.model as AIModel },
115131
messages,
116132
outputSchema: schema,
117133
});

0 commit comments

Comments
 (0)