Skip to content
Draft
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/shared/src/analytics-events.ts
Original file line number Diff line number Diff line change
Expand Up @@ -776,6 +776,7 @@ export type ChannelsSurface =
| "sidebar"
| "command_menu"
| "new_task"
| "channel_home"
| "dashboards_grid"
| "canvas"
| "context";
Expand Down
262 changes: 262 additions & 0 deletions packages/ui/src/features/canvas/components/ChannelHomeComposer.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,262 @@
import { CaretDownIcon } from "@phosphor-icons/react";
import { isValidConfigValue } from "@posthog/core/task-detail/configOptions";
import { cn } from "@posthog/quill";
import type { Task } from "@posthog/shared/domain-types";
import {
forwardRef,
useCallback,
useImperativeHandle,
useRef,
useState,
} from "react";
import { useConnectivity } from "../../../hooks/useConnectivity";
import { PromptInput } from "../../message-editor/components/PromptInput";
import { useDraftStore } from "../../message-editor/draftStore";
import type { EditorHandle } from "../../message-editor/types";
import { ReasoningLevelSelector } from "../../sessions/components/ReasoningLevelSelector";
import { UnifiedModelSelector } from "../../sessions/components/UnifiedModelSelector";
import { getCurrentModeFromConfigOptions } from "../../sessions/sessionStore";
import {
type AgentAdapter,
useSettingsStore,
} from "../../settings/settingsStore";
import {
type WorkspaceMode,
WorkspaceModeSelect,
} from "../../task-detail/components/WorkspaceModeSelect";
import { usePreviewConfig } from "../../task-detail/hooks/usePreviewConfig";
import { useTaskCreation } from "../../task-detail/hooks/useTaskCreation";

export interface ChannelHomeComposerHandle {
/** Drop a starter prompt into the editor and apply its mode, if any. */
applySuggestion: (prompt: string, mode?: string) => void;
}

interface ChannelHomeComposerProps {
channelId: string;
channelName?: string;
/** Channel CONTEXT.md, attached to the created task as background. */
channelContext?: string;
onTaskCreated: (task: Task) => void;
/** Whether the suggestion list (rendered by the parent) is open. */
suggestionsOpen: boolean;
onToggleSuggestions: () => void;
}

// The prompt box at the bottom of a channel's homepage. A trimmed-down sibling
// of TaskInput: it reuses the same task-creation pipeline (model/mode/reasoning
// preview config + useTaskCreation) but drops the repo/branch pickers — channel
// tasks run repo-less and the agent attaches a repo lazily if it needs one. The
// starter-prompt suggestions render in the parent's task list; this owns the
// local/cloud selector and the "See suggestions" toggle above the box.
export const ChannelHomeComposer = forwardRef<
ChannelHomeComposerHandle,
ChannelHomeComposerProps
>(function ChannelHomeComposer(
{
channelId,
channelName,
channelContext,
onTaskCreated,
suggestionsOpen,
onToggleSuggestions,
},
ref,
) {
const sessionId = `channel-home:${channelId}`;
const editorRef = useRef<EditorHandle>(null);
const [editorIsEmpty, setEditorIsEmpty] = useState(true);
const { isOnline } = useConnectivity();

const {
lastUsedAdapter,
setLastUsedAdapter,
lastUsedWorkspaceMode,
setLastUsedWorkspaceMode,
setLastUsedLocalWorkspaceMode,
allowBypassPermissions,
defaultInitialTaskMode,
lastUsedInitialTaskMode,
setLastUsedReasoningEffort,
setLastUsedModel,
} = useSettingsStore();

const adapter = lastUsedAdapter;
const setAdapter = useCallback(
(next: AgentAdapter) => setLastUsedAdapter(next),
[setLastUsedAdapter],
);

// Repo-less channel tasks only run local or cloud (worktree needs a repo), so
// collapse any lingering worktree preference down to local for the initial pick.
const [workspaceMode, setWorkspaceModeState] = useState<WorkspaceMode>(
lastUsedWorkspaceMode === "cloud" ? "cloud" : "local",
);
const [selectedCloudEnvId, setSelectedCloudEnvId] = useState<string | null>(
null,
);
const setWorkspaceMode = useCallback(
(mode: WorkspaceMode) => {
setWorkspaceModeState(mode);
setLastUsedWorkspaceMode(mode);
if (mode !== "cloud") setLastUsedLocalWorkspaceMode(mode);
},
[setLastUsedWorkspaceMode, setLastUsedLocalWorkspaceMode],
);

const { modeOption, modelOption, thoughtOption, isLoading, setConfigOption } =
usePreviewConfig(adapter);

const currentModel =
modelOption?.type === "select" ? modelOption.currentValue : undefined;
const adapterDefault = adapter === "codex" ? "auto" : "plan";
const modeFallback =
defaultInitialTaskMode === "last_used" &&
lastUsedInitialTaskMode &&
isValidConfigValue(modeOption, lastUsedInitialTaskMode)
? lastUsedInitialTaskMode
: adapterDefault;
const currentExecutionMode =
getCurrentModeFromConfigOptions(modeOption ? [modeOption] : undefined) ??
modeFallback;
const currentReasoningLevel =
thoughtOption?.type === "select" ? thoughtOption.currentValue : undefined;

const { isCreatingTask, canSubmit, handleSubmit } = useTaskCreation({
editorRef,
sessionId,
selectedDirectory: "",
workspaceMode,
sandboxEnvironmentId:
workspaceMode === "cloud" && selectedCloudEnvId
? selectedCloudEnvId
: undefined,
editorIsEmpty,
adapter,
executionMode: currentExecutionMode,
model: currentModel,
reasoningLevel: currentReasoningLevel,
allowNoRepo: true,
channelContext,
channelName,
onTaskCreated,
});

const handleModeChange = useCallback(
(value: string) => {
if (modeOption) setConfigOption(modeOption.id, value);
},
[modeOption, setConfigOption],
);
const handleModelChange = useCallback(
(value: string) => {
if (modelOption) {
setConfigOption(modelOption.id, value);
setLastUsedModel(value);
}
},
[modelOption, setConfigOption, setLastUsedModel],
);
const handleThoughtChange = useCallback(
(value: string) => {
if (thoughtOption) {
setConfigOption(thoughtOption.id, value);
setLastUsedReasoningEffort(value);
}
},
[thoughtOption, setConfigOption, setLastUsedReasoningEffort],
);

useImperativeHandle(
ref,
() => ({
applySuggestion: (prompt: string, mode?: string) => {
// Pending content (not setContent) preserves the multi-line template's
// line breaks and focuses at the end; mirrors the new-task screen.
useDraftStore.getState().actions.setPendingContent(sessionId, {
segments: [{ type: "text", text: prompt }],
});
if (mode && isValidConfigValue(modeOption, mode)) {
setConfigOption(modeOption.id, mode);
}
},
}),
[sessionId, modeOption, setConfigOption],
);

const hints = ["@ to add files", "/ for skills"].join(", ");

return (
<div className="mx-auto flex w-full max-w-[680px] flex-col px-4 pb-4">
<div className="mb-2 flex items-center justify-between gap-2">
<WorkspaceModeSelect
value={workspaceMode}
onChange={setWorkspaceMode}
overrideModes={["local", "cloud"]}
selectedCloudEnvironmentId={selectedCloudEnvId}
onCloudEnvironmentChange={setSelectedCloudEnvId}
size="1"
disabled={isCreatingTask}
/>
<button
type="button"
onClick={onToggleSuggestions}
className="inline-flex items-center gap-1 text-[12px] text-gray-10 transition-colors hover:text-gray-12"
>
{suggestionsOpen ? "Hide suggestions" : "See suggestions"}
<CaretDownIcon
size={12}
className={cn(
"transition-transform",
suggestionsOpen && "rotate-180",
)}
/>
</button>
</div>

<PromptInput
ref={editorRef}
sessionId={sessionId}
placeholder={`What do you want to ship? ${hints}`}
editorHeight="large"
disabled={isCreatingTask}
isLoading={isCreatingTask}
autoFocus
clearOnSubmit={false}
submitDisabledExternal={
!canSubmit || isCreatingTask || !isOnline || isLoading
}
modeOption={modeOption}
onModeChange={handleModeChange}
allowBypassPermissions={allowBypassPermissions}
enableCommands
enableBashMode={false}
modelSelector={
<UnifiedModelSelector
modelOption={modelOption}
adapter={adapter ?? "claude"}
onAdapterChange={setAdapter}
disabled={isCreatingTask}
isConnecting={isLoading}
onModelChange={handleModelChange}
/>
}
reasoningSelector={
!isLoading && (
<ReasoningLevelSelector
thoughtOption={thoughtOption}
adapter={adapter}
onChange={handleThoughtChange}
disabled={isCreatingTask}
/>
)
}
onEmptyChange={setEditorIsEmpty}
onSubmitClick={handleSubmit}
onSubmit={() => {
if (canSubmit) handleSubmit();
}}
/>
</div>
);
});
10 changes: 7 additions & 3 deletions packages/ui/src/features/canvas/components/ChannelsList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,6 @@ import {
useCreateAndOpenDashboard,
useDashboardMutations,
useDashboards,
useOpenHomeCanvas,
usePrefetchDashboards,
} from "@posthog/ui/features/canvas/hooks/useDashboards";
import { useNestedGenerationTaskIds } from "@posthog/ui/features/canvas/hooks/useNestedGenerationTaskIds";
Expand Down Expand Up @@ -957,7 +956,6 @@ function ChannelSection({
channels: Channel[];
}) {
const navigate = useNavigate();
const openHomeCanvas = useOpenHomeCanvas();
const pathname = useRouterState({ select: (s) => s.location.pathname });
const { data: tasks } = useTasks();
const archivedTaskIds = useArchivedTaskIds();
Expand Down Expand Up @@ -1102,7 +1100,13 @@ function ChannelSection({
surface: "sidebar",
channel_id: channel.id,
});
void openHomeCanvas(channel);
// Clicking the channel name expands the channel in the tree
// and opens its static homepage.
setOpen(true);
void navigate({
to: "/website/$channelId",
params: { channelId: channel.id },
});
}}
className="w-full min-w-0 justify-start ps-8 data-selected:bg-fill-selected data-selected:text-gray-12"
>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,10 @@ import { validateChannelName } from "@posthog/core/canvas/channelName";
import { Button } from "@posthog/quill";
import { ANALYTICS_EVENTS } from "@posthog/shared/analytics-events";
import { useChannelMutations } from "@posthog/ui/features/canvas/hooks/useChannels";
import { useOpenHomeCanvas } from "@posthog/ui/features/canvas/hooks/useDashboards";
import { toast } from "@posthog/ui/primitives/toast";
import { track } from "@posthog/ui/shell/analytics";
import { Dialog, Flex, IconButton, Text, TextField } from "@radix-ui/themes";
import { useNavigate } from "@tanstack/react-router";
import { useState } from "react";

// Matches Slack's "Create a channel" naming constraint.
Expand All @@ -22,7 +22,7 @@ export function CreateChannelModal({
onOpenChange,
}: CreateChannelModalProps) {
const { createChannel, isCreating } = useChannelMutations();
const openHomeCanvas = useOpenHomeCanvas();
const navigate = useNavigate();
const [name, setName] = useState("");

// Reset the field each time the modal opens so a previous draft never lingers.
Expand Down Expand Up @@ -61,9 +61,11 @@ export function CreateChannelModal({
return;
}
onOpenChange(false);
// Create + seed the channel's home canvas and open it in the main pane. A
// freshly created channel has no homeCanvasId yet, so this creates one.
await openHomeCanvas(channel);
// Open the new channel's static homepage.
void navigate({
to: "/website/$channelId",
params: { channelId: channel.id },
});
};

return (
Expand Down
Loading
Loading