Skip to content
Merged
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
6 changes: 2 additions & 4 deletions src/core/session-discovery.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import { findCocoSessionByPid } from '../services/coco-transcript.js';
import { findTraexRolloutByPid } from '../services/traex-transcript.js';
import { tmuxEnv } from '../setup/ensure-tmux.js';
import { herdrExecutable } from '../utils/herdr-executable.js';
import { LSOF_EXEC_OPTIONS } from '../utils/process-open-files.js';

// macOS 没有 /proc,所以走 ps/lsof/pgrep 兜底。Linux 仍优先走 /proc 快路径。
const IS_LINUX = platform() === 'linux';
Expand Down Expand Up @@ -359,10 +360,7 @@ export function readCwd(pid: number): string | undefined {
}
}
try {
const out = execSync(`lsof -a -d cwd -p ${pid} -Fn`, {
encoding: 'utf-8',
stdio: ['pipe', 'pipe', 'pipe'],
});
const out = execFileSync('lsof', ['-n', '-P', '-a', '-d', 'cwd', '-p', String(pid), '-Fn'], LSOF_EXEC_OPTIONS);
for (const line of out.split('\n')) {
if (line.startsWith('n')) return line.slice(1);
}
Expand Down
11 changes: 5 additions & 6 deletions src/core/worker-pool.ts
Original file line number Diff line number Diff line change
Expand Up @@ -675,6 +675,7 @@ import { isStructuredBridgeAdoptCli } from '../services/structured-bridge-clis.j
import { resolveEffectivePluginIds } from './plugins/effective.js';
import { ensureGatewayEntry } from './plugins/mcp/gateway-installer.js';
import { readPeerCrossRef } from '../services/peer-cross-ref-store.js';
import { readBotsInfoJson } from '../services/bots-info-cache.js';
import {
claimQuotaFallbackEvent,
resolveQuotaFallbackTarget,
Expand Down Expand Up @@ -2734,12 +2735,10 @@ function loadKnownBotOpenIdsForApp(larkAppId: string): Set<string> {
const dataDir = config.session.dataDir;
const crossRef = readPeerCrossRef(dataDir, larkAppId);

let botEntries: BotMentionEntry[] = [];
const botInfoPath = join(dataDir, 'bots-info.json');
if (existsSync(botInfoPath)) {
const parsed = JSON.parse(readFileSync(botInfoPath, 'utf-8'));
if (Array.isArray(parsed)) botEntries = parsed as BotMentionEntry[];
}
// Runs on every turn's final output; the cached read only re-parses when
// a sibling daemon actually rewrote bots-info.json.
const parsed = readBotsInfoJson(dataDir);
const botEntries: BotMentionEntry[] = Array.isArray(parsed) ? parsed as BotMentionEntry[] : [];

return knownBotOpenIdsFromCrossRef(crossRef, botEntries, larkAppId);
}
Expand Down
149 changes: 1 addition & 148 deletions src/dashboard/web/bot-defaults-page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ import {
import { isRemoteCliId } from '../../core/remote-cli-ids.js';
import { mountReactPage, type PageDisposer } from './react-mount.js';
import { useT } from './react-hooks.js';
import { DropdownField, ModelPickerField, type DropdownFieldOption } from './field-controls.js';
import {
MentionMock,
ModeOptionGroup,
Expand Down Expand Up @@ -489,154 +490,6 @@ function FieldTitle(props: { children: ReactNode; help?: ReactNode }) {
);
}

type DropdownFieldOption<T extends string> = {
value: T;
label: ReactNode;
hint?: ReactNode;
disabled?: boolean;
};

export function DropdownField<T extends string>(props: {
dataInput: string;
value: T;
options: DropdownFieldOption<T>[];
disabled?: boolean;
ariaLabel?: string;
className?: string;
searchable?: boolean;
onChange(value: T): void;
}) {
const tr = useT();
return (
<>
<DropdownMenu
id={`bd-menu-${props.dataInput}`}
className={['bd-field-menu', props.className].filter(Boolean).join(' ')}
ariaLabel={props.ariaLabel}
disabled={props.disabled}
label={dropdownLabel(props.options, props.value)}
value={props.value}
options={props.options}
searchable={props.searchable}
searchPlaceholder={props.searchable ? tr('common.dropdownSearch') : undefined}
searchEmptyLabel={props.searchable ? tr('common.dropdownSearchEmpty') : undefined}
onChange={props.onChange}
/>
<input type="hidden" data-input={props.dataInput} value={props.value} readOnly />
</>
);
}

const MODEL_PICKER_CUSTOM = '__custom__';

/**
* 模型选择器:下拉候选(静态精选 + live 探测合并,由调用方 mergeModelCandidates 算好)
* + 「自定义模型…」自由输入。模型列表会过期,候选永不锁死。
* - value='' 表示跟随 CLI 默认,菜单显示 defaultLabel;
* - value 非空但不在候选中时,把当前值作为额外选项插在最前(旧配置/自定义值可见);
* - 选中 customLabel 切到自定义输入模式(datalist 仍挂全部候选做自动补全),
* 返回按钮切回下拉模式;
* - busy(live 探测进行中)只在控件下方显示小转圈,不禁用选择。
*/
export function ModelPickerField(props: {
value: string;
onChange(next: string): void;
options: readonly string[];
disabled?: boolean;
busy?: boolean;
dataInput: string;
ariaLabel: string;
defaultLabel: string;
customLabel: string;
includeDefault?: boolean;
detectedCount?: number;
detectedLabel?: string;
/** 下拉菜单样式类:defaults 页传 bd-field-menu,onboarding 传 onboarding-menu。 */
menuClassName?: string;
}): React.JSX.Element {
const tr = useT();
const [customMode, setCustomMode] = useState(false);
const datalistId = useId();
const current = props.value;
const dropdownOptions = useMemo(() => {
const opts: { value: string; label: ReactNode }[] = [];
if (props.includeDefault) opts.push({ value: '', label: props.defaultLabel });
if (current && !props.options.includes(current)) {
opts.push({ value: current, label: current });
}
for (const item of props.options) opts.push({ value: item, label: item });
opts.push({ value: MODEL_PICKER_CUSTOM, label: props.customLabel });
return opts;
}, [current, props.options, props.customLabel, props.includeDefault, props.defaultLabel]);

return (
<span className="bd-model-picker">
{customMode ? (
<span className="bd-model-custom">
<input
type="text"
data-input={props.dataInput}
list={datalistId}
value={current}
placeholder={props.defaultLabel}
disabled={props.disabled}
autoComplete="off"
autoCapitalize="none"
spellCheck={false}
onChange={event => props.onChange(event.currentTarget.value)}
/>
<button
type="button"
className="bd-model-back"
disabled={props.disabled}
onClick={() => setCustomMode(false)}
>
{tr('botDefaults.modelPickerBack')}
</button>
<datalist id={datalistId}>
{props.options.map(item => <option value={item} key={item} />)}
</datalist>
</span>
) : (
<>
<DropdownMenu<string>
id={`bd-menu-${props.dataInput}`}
className={['bd-model-menu', props.menuClassName].filter(Boolean).join(' ')}
ariaLabel={props.ariaLabel}
disabled={props.disabled}
label={current || props.defaultLabel}
value={current}
options={dropdownOptions}
searchable
searchPlaceholder={tr('common.dropdownSearch')}
searchEmptyLabel={tr('common.dropdownSearchEmpty')}
onChange={next => {
if (next === MODEL_PICKER_CUSTOM) {
setCustomMode(true);
return;
}
props.onChange(next);
}}
/>
{/* 与 DropdownField 同款 data-input 锚点:既有测试/自动化经它读写当前值 */}
<input
type="hidden"
data-input={props.dataInput}
value={current}
onChange={event => props.onChange(event.currentTarget.value)}
/>
</>
)}
{props.busy
? <small className="bd-model-busy"><span className="bd-model-spinner" aria-hidden="true" /></small>
: null}
{typeof props.detectedCount === 'number' && props.detectedCount > 0 && props.detectedLabel
? <small className="bd-model-detected">{props.detectedLabel}</small>
: null}
</span>
);
}

function ToggleRow(props: {
checked: boolean;
disabled?: boolean;
Expand Down
2 changes: 1 addition & 1 deletion src/dashboard/web/bot-onboarding.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import type React from 'react';
import { useCallback, useEffect, useMemo, useRef, useState, type FormEvent } from 'react';
import { DropdownMenu } from './dashboard-components.js';
import { fetchDetectedModels, mergeModelCandidates, modelSuggestionsForOption } from './bot-defaults.js';
import { ModelPickerField } from './bot-defaults-page.js';
import { ModelPickerField } from './field-controls.js';
import { confirm } from './confirm-modal.js';
import { t } from './ui.js';

Expand Down
155 changes: 155 additions & 0 deletions src/dashboard/web/field-controls.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,155 @@
import { useId, useMemo, useState, type ReactNode } from 'react';
import { DropdownMenu, dropdownLabel } from './dashboard-components.js';
import { useT } from './react-hooks.js';

// Form controls shared by the bot defaults page, bot onboarding and group
// default models. Kept out of bot-defaults-page.tsx so the statically loaded
// onboarding flow does not pull that whole (lazy-loaded) page into app.js.

export type DropdownFieldOption<T extends string> = {
value: T;
label: ReactNode;
hint?: ReactNode;
disabled?: boolean;
};

export function DropdownField<T extends string>(props: {
dataInput: string;
value: T;
options: DropdownFieldOption<T>[];
disabled?: boolean;
ariaLabel?: string;
className?: string;
searchable?: boolean;
onChange(value: T): void;
}) {
const tr = useT();
return (
<>
<DropdownMenu
id={`bd-menu-${props.dataInput}`}
className={['bd-field-menu', props.className].filter(Boolean).join(' ')}
ariaLabel={props.ariaLabel}
disabled={props.disabled}
label={dropdownLabel(props.options, props.value)}
value={props.value}
options={props.options}
searchable={props.searchable}
searchPlaceholder={props.searchable ? tr('common.dropdownSearch') : undefined}
searchEmptyLabel={props.searchable ? tr('common.dropdownSearchEmpty') : undefined}
onChange={props.onChange}
/>
<input type="hidden" data-input={props.dataInput} value={props.value} readOnly />
</>
);
}

const MODEL_PICKER_CUSTOM = '__custom__';

/**
* 模型选择器:下拉候选(静态精选 + live 探测合并,由调用方 mergeModelCandidates 算好)
* + 「自定义模型…」自由输入。模型列表会过期,候选永不锁死。
* - value='' 表示跟随 CLI 默认,菜单显示 defaultLabel;
* - value 非空但不在候选中时,把当前值作为额外选项插在最前(旧配置/自定义值可见);
* - 选中 customLabel 切到自定义输入模式(datalist 仍挂全部候选做自动补全),
* 返回按钮切回下拉模式;
* - busy(live 探测进行中)只在控件下方显示小转圈,不禁用选择。
*/
export function ModelPickerField(props: {
value: string;
onChange(next: string): void;
options: readonly string[];
disabled?: boolean;
busy?: boolean;
dataInput: string;
ariaLabel: string;
defaultLabel: string;
customLabel: string;
includeDefault?: boolean;
detectedCount?: number;
detectedLabel?: string;
/** 下拉菜单样式类:defaults 页传 bd-field-menu,onboarding 传 onboarding-menu。 */
menuClassName?: string;
}): React.JSX.Element {
const tr = useT();
const [customMode, setCustomMode] = useState(false);
const datalistId = useId();
const current = props.value;
const dropdownOptions = useMemo(() => {
const opts: { value: string; label: ReactNode }[] = [];
if (props.includeDefault) opts.push({ value: '', label: props.defaultLabel });
if (current && !props.options.includes(current)) {
opts.push({ value: current, label: current });
}
for (const item of props.options) opts.push({ value: item, label: item });
opts.push({ value: MODEL_PICKER_CUSTOM, label: props.customLabel });
return opts;
}, [current, props.options, props.customLabel, props.includeDefault, props.defaultLabel]);

return (
<span className="bd-model-picker">
{customMode ? (
<span className="bd-model-custom">
<input
type="text"
data-input={props.dataInput}
list={datalistId}
value={current}
placeholder={props.defaultLabel}
disabled={props.disabled}
autoComplete="off"
autoCapitalize="none"
spellCheck={false}
onChange={event => props.onChange(event.currentTarget.value)}
/>
<button
type="button"
className="bd-model-back"
disabled={props.disabled}
onClick={() => setCustomMode(false)}
>
{tr('botDefaults.modelPickerBack')}
</button>
<datalist id={datalistId}>
{props.options.map(item => <option value={item} key={item} />)}
</datalist>
</span>
) : (
<>
<DropdownMenu<string>
id={`bd-menu-${props.dataInput}`}
className={['bd-model-menu', props.menuClassName].filter(Boolean).join(' ')}
ariaLabel={props.ariaLabel}
disabled={props.disabled}
label={current || props.defaultLabel}
value={current}
options={dropdownOptions}
searchable
searchPlaceholder={tr('common.dropdownSearch')}
searchEmptyLabel={tr('common.dropdownSearchEmpty')}
onChange={next => {
if (next === MODEL_PICKER_CUSTOM) {
setCustomMode(true);
return;
}
props.onChange(next);
}}
/>
{/* 与 DropdownField 同款 data-input 锚点:既有测试/自动化经它读写当前值 */}
<input
type="hidden"
data-input={props.dataInput}
value={current}
onChange={event => props.onChange(event.currentTarget.value)}
/>
</>
)}
{props.busy
? <small className="bd-model-busy"><span className="bd-model-spinner" aria-hidden="true" /></small>
: null}
{typeof props.detectedCount === 'number' && props.detectedCount > 0 && props.detectedLabel
? <small className="bd-model-detected">{props.detectedLabel}</small>
: null}
</span>
);
}
2 changes: 1 addition & 1 deletion src/dashboard/web/group-default-models.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import React, { useEffect, useRef, useState } from 'react';
import { groupModelSettings, type GroupDefaultModels, type GroupModelSettings } from '../../core/group-default-models.js';
import { reasoningEffortsForCliModel } from '../../services/codex-reasoning-effort.js';
import { useT } from './react-hooks.js';
import { DropdownField, ModelPickerField } from './bot-defaults-page.js';
import { DropdownField, ModelPickerField } from './field-controls.js';
import { fetchCliOptions, fetchDetectedModels, mergeModelCandidates, modelSuggestionsForOption, selectedCliOption } from './bot-defaults.js';

type Props = {
Expand Down
Loading
Loading