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
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ const PAGE_PROJECT_KEYS: Record<SettingsPage, readonly ProjectScopedServerSettin
threads: [
"defaultThreadEnvMode",
"defaultRuntimeMode",
"sideChatRuntimeMode",
"responseStreamingMode",
"enableAgentBrowserAccess",
"sidebarAutoSettleOnMerge",
Expand Down Expand Up @@ -272,6 +273,15 @@ function ServerSettingsDetail(props: { readonly page: SettingsPage }) {
RUNTIME_MODE_CHOICES,
targets.map((target) => target.environment.serverConfig.managedPolicy),
);
const sideChatRuntimeModeChoices = [
...runtimeModeChoices,
{
mode: "inherit-parent" as const,
label: "Inherit parent",
description: "Use the parent’s current permissions, within administrator limits.",
disabledReason: undefined,
},
];
const disabledFor = (key: string) =>
disabled ||
managed(key) !== undefined ||
Expand Down Expand Up @@ -337,29 +347,43 @@ function ServerSettingsDetail(props: { readonly page: SettingsPage }) {
/>
))}
</SettingsSection>
<SettingsSection
title="Default permissions"
footer={managed("defaultRuntimeMode")}
trailing={
pendingWrites === 0 && uniform("defaultRuntimeMode") === null ? (
<MixedValuesLabel projectSelected={projectSelected} />
) : null
}
>
{runtimeModeChoices.map((choice, index) => (
<SettingsChoiceRow
key={choice.mode}
label={choice.label}
description={choice.disabledReason ?? choice.description}
selected={uniform("defaultRuntimeMode") === choice.mode}
separated={index > 0}
disabled={
disabledFor("defaultRuntimeMode") || choice.disabledReason !== undefined
}
onPress={() => write({ defaultRuntimeMode: choice.mode })}
/>
))}
</SettingsSection>
{(
[
["defaultRuntimeMode", "Permissions · New threads"],
["sideChatRuntimeMode", "Permissions · Side chats"],
] as const
).map(([key, title]) => (
<SettingsSection
key={key}
title={title}
footer={
managed(key) ??
(key === "sideChatRuntimeMode"
? "Permissions for new /side chats. Side chats share the parent’s worktree."
: undefined)
}
trailing={
pendingWrites === 0 && uniform(key) === null ? (
<MixedValuesLabel projectSelected={projectSelected} />
) : null
}
>
{(key === "sideChatRuntimeMode"
? sideChatRuntimeModeChoices
: runtimeModeChoices
).map((choice, index) => (
<SettingsChoiceRow
key={choice.mode}
label={choice.label}
description={choice.disabledReason ?? choice.description}
selected={uniform(key) === choice.mode}
separated={index > 0}
disabled={disabledFor(key) || choice.disabledReason !== undefined}
onPress={() => write({ [key]: choice.mode })}
/>
))}
</SettingsSection>
))}
<SettingsSection
title="Response streaming"
footer={managed("responseStreamingMode")}
Expand Down
32 changes: 5 additions & 27 deletions apps/mobile/src/features/threads/useStartSideChat.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,7 @@ import {
squashAtomCommandFailure,
type AtomCommandResult,
} from "@t3tools/client-runtime/state/runtime";
import {
MessageId,
ThreadId,
type ModelSelection,
type RuntimeMode,
type ScopedThreadRef,
} from "@t3tools/contracts";
import { MessageId, ThreadId, type ModelSelection, type ScopedThreadRef } from "@t3tools/contracts";

import * as Dialog from "../../components/ui/Dialog";
import { uuidv4 } from "../../lib/uuid";
Expand All @@ -23,13 +17,12 @@ import { useOpenLinkedThread } from "./useOpenLinkedThread";

/**
* Forks `source` at its latest stable point into a side chat, sends `prompt` there, and opens
* it like any linked thread. Resolves false when the side chat did not start. `settings` are
* what the parent's composer would send, which its thread gets only on its next send.
* it like any linked thread with server-resolved permissions and the parent's model.
* Resolves false when the side chat did not start.
*/
export function useStartSideChat() {
const forkSideChat = useAtomCommand(threadEnvironment.forkSideChat, { reportFailure: false });
const deleteThread = useAtomCommand(threadEnvironment.delete, { reportFailure: false });
const setRuntimeMode = useAtomCommand(threadEnvironment.setRuntimeMode, { reportFailure: false });
const startTurn = useAtomCommand(threadEnvironment.startTurn, { reportFailure: false });
const openLinkedThread = useOpenLinkedThread();

Expand All @@ -45,7 +38,7 @@ export function useStartSideChat() {
return async (
source: ScopedThreadRef,
prompt: string,
settings: { readonly runtimeMode: RuntimeMode; readonly modelSelection: ModelSelection },
settings: { readonly modelSelection: ModelSelection },
): Promise<boolean> => {
const targetThreadId = ThreadId.make(uuidv4());
const fork = await forkSideChat({
Expand Down Expand Up @@ -82,21 +75,6 @@ export function useStartSideChat() {
discard();
return false;
}
if (settings.runtimeMode !== sideChat.runtimeMode) {
const mode = await setRuntimeMode({
environmentId: source.environmentId,
input: {
threadId: targetThreadId,
runtimeMode: settings.runtimeMode,
createdAt: new Date().toISOString(),
},
});
if (mode._tag === "Failure") {
alertFailure(mode, "Could not set the side chat's permission mode.");
discard();
return false;
}
}
const turn = await startTurn({
environmentId: source.environmentId,
input: {
Expand All @@ -109,7 +87,7 @@ export function useStartSideChat() {
},
modelSelection: settings.modelSelection,
titleSeed: deriveThreadTitleSeed({ text: prompt, attachments: [] }),
runtimeMode: settings.runtimeMode,
runtimeMode: sideChat.runtimeMode,
interactionMode: "default",
createdAt: new Date().toISOString(),
},
Expand Down
2 changes: 1 addition & 1 deletion apps/mobile/src/state/thread-task-permissions.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -368,7 +368,7 @@ describe("mobile task permissions", () => {
expect(startSideChat).toHaveBeenCalledExactlyOnceWith(
{ environmentId: "secondary", threadId: "thread" },
"why does this fail?",
{ runtimeMode: "approval-required", modelSelection: selection },
{ modelSelection: selection },
);
expect(state.enqueue).not.toHaveBeenCalled();
expect(state.clearDraft).toHaveBeenCalled();
Expand Down
4 changes: 1 addition & 3 deletions apps/mobile/src/state/use-thread-composer-state.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,6 @@ import {
orchestrationV2WorktreePending,
type EnvironmentId,
type ModelSelection,
type RuntimeMode,
type ScopedThreadRef,
type ThreadId,
} from "@t3tools/contracts";
Expand Down Expand Up @@ -182,7 +181,7 @@ export function useThreadComposerState(input: {
readonly startSideChat: (
source: ScopedThreadRef,
prompt: string,
settings: { readonly runtimeMode: RuntimeMode; readonly modelSelection: ModelSelection },
settings: { readonly modelSelection: ModelSelection },
) => Promise<boolean>;
}) {
const {
Expand Down Expand Up @@ -577,7 +576,6 @@ export function useThreadComposerState(input: {
{ environmentId: thread.environmentId, threadId: thread.id },
sideChatCommand.prompt,
{
runtimeMode: draft.runtimeMode ?? thread.runtimeMode,
modelSelection,
},
)
Expand Down
30 changes: 29 additions & 1 deletion apps/server/src/orchestration-v2/Orchestrator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,7 @@ import {
managedRuntimeModeCeilingMessage,
} from "@t3tools/contracts";
import { modelSelectionsEqual } from "@t3tools/shared/model";
import { resolveProjectSettings } from "@t3tools/shared/projectSettings";
import { modelSwitchSince } from "./modelSwitch.ts";
import { recordThreadOpen } from "@t3tools/shared/paletteSearch";
import {
Expand Down Expand Up @@ -4884,6 +4885,28 @@ const makeOrchestrator = Effect.fn("orchestrationV2.Orchestrator.layer")(functio
type: "fork",
}),
);
const sideChatRuntimeMode = command.sideChat
? resolveProjectSettings(
yield* serverSettings.getSettings.pipe(mapDispatchError(command)),
sourceProjection.thread.projectId,
).settings.sideChatRuntimeMode
: undefined;
const selectedRuntimeMode =
sideChatRuntimeMode === "inherit-parent"
? sourceProjection.thread.runtimeMode
: sideChatRuntimeMode;
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Resolved
const runtimeMode =
selectedRuntimeMode !== undefined &&
runtimeModeCeiling !== undefined &&
isRuntimeModeAbove(selectedRuntimeMode, runtimeModeCeiling.maxRuntimeMode)
? runtimeModeCeiling.maxRuntimeMode
: selectedRuntimeMode;
if (runtimeMode !== undefined) {
yield* refuseAboveDispatchModeLimit(command, command.sourceThreadId, {
runtimeMode,
interactionMode: sourceProjection.thread.interactionMode,
});
}
const { targetThread, transfer } = yield* threadForkService
.plan({
sourceProjection,
Expand All @@ -4900,7 +4923,12 @@ const makeOrchestrator = Effect.fn("orchestrationV2.Orchestrator.layer")(functio
transferId,
targetThreadId: command.targetThreadId,
...(command.title === undefined ? {} : { title: command.title }),
...(command.sideChat ? { sideChat: true } : {}),
...(runtimeMode === undefined
? {}
: {
sideChat: true,
sideChatRuntimeMode: runtimeMode,
}),
createdBy: command.createdBy,
creationSource: command.creationSource,
createdAt: now,
Expand Down
Loading
Loading