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
40 changes: 39 additions & 1 deletion apps/app/src/app/lib/desktop.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,17 @@ export type BrowserProxyState = {
proxy: { rules: string; authenticated: boolean } | null;
};

export type LanPreviewState = {
enabled: boolean;
port: number;
addresses: string[];
code: string | null;
codeExpiresAt: number;
sessionCount: number;
pendingChallengeCount?: number;
error?: string;
};

// ---------------------------------------------------------------------------
// Electron bridge surface
// ---------------------------------------------------------------------------
Expand Down Expand Up @@ -154,13 +165,40 @@ declare global {
onPanelClosed?: (callback: () => void) => () => void;
};
terminal?: {
create?: (options: { cwd: string; cols: number; rows: number }) => Promise<{ terminalId: string }>;
create?: (options: {
cwd: string;
cols: number;
rows: number;
/** Optional argv for the shell. When present the shell runs this
command instead of opening an interactive prompt, e.g.
`["ssh", "user@host"]` for the ops panel. */
command?: string[];
/** Optional explicit shell/executable path. */
shell?: string;
}) => Promise<{ terminalId: string }>;
write?: (terminalId: string, data: string) => Promise<void>;
resize?: (terminalId: string, cols: number, rows: number) => Promise<void>;
kill?: (terminalId: string) => Promise<void>;
onData?: (callback: (payload: { terminalId: string; data: string }) => void) => () => void;
onExit?: (callback: (payload: { terminalId: string; exitCode: number | null; signal?: number }) => void) => () => void;
};
ssh?: {
listHosts?: () => Promise<{ hosts: string[]; configPath: string }>;
};
git?: {
graph?: (options: { cwd: string; maxCommits?: number }) => Promise<
| { ok: true; repoRoot: string; count: number; totalCount: number | null; truncated: boolean; isRepo: true; commits: { sha: string; parents: string[] }[]; refs: { sha: string; refname: string; head: boolean }[]; headShas: string[] }
| { ok: false; isRepo: boolean; error: string }
>;
};
lanPreview?: {
getState?: () => Promise<LanPreviewState>;
setEnabled?: (enabled: boolean) => Promise<LanPreviewState>;
regenerateCode?: () => Promise<LanPreviewState>;
disconnectAll?: () => Promise<LanPreviewState>;
pushToIm?: (options: { mcpUrl: string }) => Promise<{ ok: boolean; tool?: string; error?: string }>;
onStateChanged?: (callback: (state: LanPreviewState) => void) => () => void;
};
hyperframes?: {
start?: (options: { workspaceRoot: string; sessionId: string; projectDirectory: string; port: number }) => Promise<{ ok: boolean; port?: number; reused?: boolean }>;
stop?: (sessionId: string, options?: { keepWarm?: boolean }) => Promise<{ ok: boolean }>;
Expand Down
1 change: 1 addition & 0 deletions apps/app/src/app/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -192,6 +192,7 @@ export const SETTINGS_TAB_VALUES = [
"updates",
"recovery",
"debug",
"remote-preview",
] as const;

export type SettingsTab = (typeof SETTINGS_TAB_VALUES)[number];
Expand Down
39 changes: 36 additions & 3 deletions apps/app/src/i18n/locales/en.ts
Original file line number Diff line number Diff line change
Expand Up @@ -408,9 +408,42 @@ export default {
"design_system.embedded.font_inter": "Inter",
"design_system.embedded.font_georgia": "Georgia",
"design_system.embedded.font_ibm_plex_sans": "IBM Plex Sans",
"template_market.title": "Templates",
"template_market.description": "Browse installed and bundled templates for design and video tasks.",
"template_market.search_placeholder": "Search templates",
"template_market.title": "Templates",
"template_market.description": "Browse installed and bundled templates for design and video tasks.",
"template_market.search_placeholder": "Search templates",
"ops.title": "Ops",
"git.title": "Git",
"settings.tab_remote_preview": "Remote Preview",
"settings.tab_description_remote_preview": "Allow LAN devices to view this workbench (read-only).",
"settings.remote_preview_alert_title": "Remote preview (LAN read-only)",
"settings.remote_preview_alert_desc": "Lets phones and tablets on your local network view the current workbench state through a browser. Read-only: devices can watch, not control.",
"settings.remote_preview_desktop_title": "Desktop app required",
"settings.remote_preview_desktop_desc": "Remote preview is only available in the desktop app.",
"settings.remote_preview_enable_title": "Enable LAN preview",
"settings.remote_preview_enable_desc": "Defaults to off. Turning it on exposes a read-only snapshot to your local network.",
"settings.remote_preview_address_title": "Access address",
"settings.remote_preview_address_desc": "Open this address in a phone or tablet browser on the same network.",
"settings.remote_preview_no_address": "No LAN addresses found.",
"settings.remote_preview_code_title": "Pair code",
"settings.remote_preview_code_desc": "Valid for 10 minutes, single use",
"settings.remote_preview_regenerate": "Regenerate",
"settings.remote_preview_code_hint": "Enter this code on the device to pair it with this workbench.",
"settings.remote_preview_sessions_title": "Paired devices",
"settings.remote_preview_sessions_connected": "devices connected",
"settings.remote_preview_sessions_none": "No devices paired.",
"settings.remote_preview_disconnect_all": "Disconnect all",
"settings.remote_preview_error_title": "Could not start remote preview",
"settings.remote_preview_copy": "Copy",
"settings.remote_preview_im_title": "Push preview to IM",
"settings.remote_preview_im_desc": "Send the current workbench status to a DingTalk/Feishu group via its MCP endpoint.",
"settings.remote_preview_im_endpoint_placeholder": "https://… DingTalk MCP endpoint (dws mcp url get <mcpId>)",
"settings.remote_preview_im_endpoint_label": "DingTalk MCP endpoint URL",
"settings.remote_preview_im_hint": "Get the endpoint from `dws mcp url get <mcpId>`. Only a redacted status summary is sent; no session tokens.",
"settings.remote_preview_im_push": "Push summary",
"settings.remote_preview_im_sent": "Preview summary sent.",
"settings.remote_preview_im_failed": "Could not send the preview summary.",
"settings.remote_preview_im_unavailable": "IM push is only available in the desktop app.",
"settings.remote_preview_im_need_endpoint": "Enter a DingTalk MCP endpoint first.",
"template_market.my_templates": "My templates",
"template_market.import_package": "Import .ipwp or .ipwt",
"template_market.all_templates": "All templates",
Expand Down
37 changes: 35 additions & 2 deletions apps/app/src/i18n/locales/zh.ts
Original file line number Diff line number Diff line change
Expand Up @@ -410,8 +410,41 @@ export default {
"design_system.embedded.font_inter": "Inter",
"design_system.embedded.font_georgia": "Georgia",
"design_system.embedded.font_ibm_plex_sans": "IBM Plex Sans",
"template_market.title": "模版",
"template_market.description": "浏览设计和视频任务可用的内置、已安装和本地模板。",
"template_market.title": "模版",
"template_market.description": "浏览设计和视频任务可用的内置、已安装和本地模板。",
"ops.title": "运维",
"git.title": "Git",
"settings.tab_remote_preview": "远程预览",
"settings.tab_description_remote_preview": "允许局域网设备查看此工作台(只读)。",
"settings.remote_preview_alert_title": "远程预览(局域网只读)",
"settings.remote_preview_alert_desc": "让同一局域网内的手机、平板通过浏览器查看当前工作台状态。只读:设备只能观看,不能操作。",
"settings.remote_preview_desktop_title": "需要桌面应用",
"settings.remote_preview_desktop_desc": "远程预览仅在桌面应用内可用。",
"settings.remote_preview_enable_title": "启用局域网预览",
"settings.remote_preview_enable_desc": "默认关闭。开启后会向局域网暴露只读快照。",
"settings.remote_preview_address_title": "访问地址",
"settings.remote_preview_address_desc": "在手机或平板浏览器中打开该地址(需同一网络)。",
"settings.remote_preview_no_address": "未找到局域网地址。",
"settings.remote_preview_code_title": "配对码",
"settings.remote_preview_code_desc": "10 分钟有效,单次使用",
"settings.remote_preview_regenerate": "重新生成",
"settings.remote_preview_code_hint": "在设备上输入此配对码即可与工作台配对。",
"settings.remote_preview_sessions_title": "已配对设备",
"settings.remote_preview_sessions_connected": "台设备已连接",
"settings.remote_preview_sessions_none": "暂无已配对设备。",
"settings.remote_preview_disconnect_all": "断开全部",
"settings.remote_preview_error_title": "远程预览启动失败",
"settings.remote_preview_copy": "复制",
"settings.remote_preview_im_title": "推送预览到 IM",
"settings.remote_preview_im_desc": "通过 MCP 端点将当前工作台状态发送到钉钉/飞书群。",
"settings.remote_preview_im_endpoint_placeholder": "https://… 钉钉 MCP 端点(dws mcp url get <mcpId>)",
"settings.remote_preview_im_endpoint_label": "钉钉 MCP 端点 URL",
"settings.remote_preview_im_hint": "用 `dws mcp url get <mcpId>` 获取端点。仅发送脱敏后的状态摘要,不包含任何会话令牌。",
"settings.remote_preview_im_push": "推送摘要",
"settings.remote_preview_im_sent": "预览摘要已发送。",
"settings.remote_preview_im_failed": "发送预览摘要失败。",
"settings.remote_preview_im_unavailable": "IM 推送仅在桌面应用中可用。",
"settings.remote_preview_im_need_endpoint": "请先输入钉钉 MCP 端点。",
"template_market.search_placeholder": "搜索模板",
"template_market.my_templates": "我的模板",
"template_market.import_package": "导入 .ipwp 或 .ipwt",
Expand Down
26 changes: 23 additions & 3 deletions apps/app/src/react-app/domains/session/chat/session-page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,8 @@ import { shouldRefreshTemplateCatalogOnOpen } from "../templates/template-market
import { savePromptTemplate } from "@/react-app/domains/session/templates/prompt-template-store";
import { SidePanel, type SidePanelLauncherItem } from "../panel/side-panel";
import { TerminalDock } from "../terminal/terminal-dock";
import { OpsPanel } from "../ops/ops-panel";
import { GitPanel } from "../git/git-panel";
import { useActivePanelTab, usePanelTabStore, useSessionPanelState } from "../panel/panel-tab-store";
import { useWorkspaceShellLayout } from "../../../shell/workspace-shell-layout";
import { useControlAction, type iPolloWorkControlAction } from "../../../shell/control/control-provider";
Expand Down Expand Up @@ -1235,7 +1237,7 @@ export function SessionPage(props: SessionPageProps) {
const [renameGroupTarget, setRenameGroupTarget] = useState<{ workspaceId: string; groupId: string } | null>(null);
const [removeGroupOpen, setRemoveGroupOpen] = useState(false);
const [removeGroupTarget, setRemoveGroupTarget] = useState<{ workspaceId: string; groupId: string; label: string } | null>(null);
const [mainWorkspaceView, setMainWorkspaceView] = useState<"extensions" | null>(null);
const [mainWorkspaceView, setMainWorkspaceView] = useState<"extensions" | "ops" | "git" | null>(null);
const preserveSidePanelOnPanelOpenRef = useRef(false);

const setCurrentSidePanel = useCallback((panel: SidePanelItem | null) => {
Expand Down Expand Up @@ -2080,6 +2082,14 @@ export function SessionPage(props: SessionPageProps) {
setCurrentSidePanel(null);
setMainWorkspaceView("extensions");
}, [setCurrentSidePanel]);
const openOpsRailPane = useCallback(() => {
setCurrentSidePanel(null);
setMainWorkspaceView("ops");
}, [setCurrentSidePanel]);
const openGitRailPane = useCallback(() => {
setCurrentSidePanel(null);
setMainWorkspaceView("git");
}, [setCurrentSidePanel]);
const openVoiceRailPane = useCallback(() => {
toggleCurrentSidePanel("voice");
}, [toggleCurrentSidePanel]);
Expand Down Expand Up @@ -2305,7 +2315,7 @@ export function SessionPage(props: SessionPageProps) {
(showWorkspaceSetupEmptyState || (props.selectedSessionId && !selectedSessionIsDefaultTitle)),
);
const showMainHeaderMenu = showHeaderMenu && showMainHeaderTitle;
const mainHeaderHidden = mainWorkspaceView === "extensions" || (showNewConversationChrome && !sidebarVisuallyCollapsed);
const mainHeaderHidden = mainWorkspaceView === "extensions" || mainWorkspaceView === "ops" || mainWorkspaceView === "git" || (showNewConversationChrome && !sidebarVisuallyCollapsed);
const visibleWorkspaceWidth = viewportWidth - (shellConfig.sidebar && sidebarOpen ? effectiveLeftSidebarWidth : 0);
const floatingRightPanelToggleOffset = sidePanelOpen
? Math.min(effectiveBrowserPanelWidth, Math.max(0, visibleWorkspaceWidth - 40)) + 8
Expand Down Expand Up @@ -2422,12 +2432,14 @@ export function SessionPage(props: SessionPageProps) {
name: denAuth.user?.name ?? null,
email: denAuth.user?.email ?? null,
}}
activePrimaryItem={templateMarketOpen ? "template-market" : mainWorkspaceView === "extensions" ? "extensions" : null}
activePrimaryItem={templateMarketOpen ? "template-market" : mainWorkspaceView === "extensions" ? "extensions" : mainWorkspaceView === "ops" ? "ops" : mainWorkspaceView === "git" ? "git" : null}
onOpenAccount={openCloudAccount}
onOpenSettings={props.onOpenSettings}
onOpenHelp={props.onOpenHelp}
onOpenTemplateMarket={() => setTemplateMarketOpen(true)}
onOpenExtensions={openExtensionsRailPane}
onOpenOps={openOpsRailPane}
onOpenGit={openGitRailPane}
onSignIn={openCloudSignIn}
onOpenSessionSearch={props.sidebar.onOpenSessionSearch ? handleSidebarOpenSessionSearch : undefined}
onStartResize={startLeftSidebarResize}
Expand Down Expand Up @@ -2620,6 +2632,14 @@ export function SessionPage(props: SessionPageProps) {
<div className="flex h-full min-h-0 flex-col overflow-y-auto bg-background">
{props.settingsSlot}
</div>
) : mainWorkspaceView === "ops" ? (
<div className="flex h-full min-h-0 flex-col bg-background">
<OpsPanel onClose={() => setMainWorkspaceView(null)} />
</div>
) : mainWorkspaceView === "git" ? (
<div className="flex h-full min-h-0 flex-col bg-background">
<GitPanel workspaceRoot={props.selectedWorkspaceRoot} onClose={() => setMainWorkspaceView(null)} />
</div>
) : showStartupSkeleton ? (
<div className="px-6 py-14" role="status" aria-live="polite">
<div className="mx-auto max-w-2xl space-y-6">
Expand Down
Loading