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: 6 additions & 0 deletions docs/en/user-guide/coding/git-workspace-projects.md
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,12 @@ Select **Files and folders** to open the native system picker. The macOS desktop

Workspace search is restricted to the execution directory bound to the current task or project. The native picker can reference local paths outside that workspace, but those paths are useful only to a local execution device that can access them; remote and cloud tasks cannot rely on paths from the user's computer. Previously sent path references remain visible in message history.

## Use Quick Phrases

Select **Quick phrases** in the lower-left area of the Wework composer to open the phrases stored on this device. Choose a phrase with the pointer, or type in the search field and use the arrow keys to move, `Enter` to confirm, and `Esc` to close. Wework appends the selected phrase to the current draft on a new line; it does not send the message immediately.

Open **Settings > Quick phrases** to create, edit, delete, and reorder phrases. Each phrase can use Normal, Plan mode, or Goal; selecting it also switches the composer to that mode. Quick phrases are stored on the current device, remain available offline, and are not synchronized to other devices.

## Commit and Push Changes

In desktop Wework, open **Environment info** in the upper-right corner, then select **Commit or push** to run one of these actions in the current task or project's actual workspace directory:
Expand Down
6 changes: 6 additions & 0 deletions docs/zh/user-guide/coding/git-workspace-projects.md
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,12 @@ git config user.email <git_email>

工作区搜索只在当前任务或项目绑定的执行目录内进行。系统选择器可以引用工作区外的本机路径,但只适用于能够访问这些路径的本地执行设备;远程或云端任务不能依赖用户电脑上的本地路径。历史消息会继续显示已经发送的路径引用。

## 使用快捷短语

Wework 输入框左下角的“快捷短语”按钮会打开本机短语列表。可以用鼠标选择,也可以在搜索框中输入关键词后用方向键移动、`Enter` 确认、`Esc` 关闭。选中后,短语会换行追加到当前草稿,不会立即发送。

在“设置 > 快捷短语”中可以新建、编辑、删除和排序短语。每条短语可设为普通、计划模式或 Goal;选择时 Wework 会同步切换到该模式。快捷短语保存在当前设备,可离线使用,不会同步到其他设备。

## 提交和推送更改

在桌面版 Wework 中,打开右上角“环境信息”,然后点击“提交或推送”,可以在当前任务或项目的实际工作目录中执行以下操作:
Expand Down
43 changes: 43 additions & 0 deletions wework/src-tauri/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -401,6 +401,40 @@ struct AppPreferences {
browser_ask_before_download: bool,
#[serde(default = "default_true")]
appshots_play_sound: bool,
#[serde(default = "default_quick_phrases")]
quick_phrases: Vec<QuickPhrase>,
}

#[derive(Clone, serde::Deserialize, serde::Serialize)]
#[serde(rename_all = "camelCase")]
struct QuickPhrase {
id: String,
title: String,
content: String,
mode: String,
}

fn default_quick_phrases() -> Vec<QuickPhrase> {
vec![
QuickPhrase {
id: "default-summary-progress".into(),
title: "总结当前进展".into(),
content: "总结目前完成的工作和下一步建议".into(),
mode: "normal".into(),
},
QuickPhrase {
id: "default-create-plan".into(),
title: "制定实施计划".into(),
content: "分析需求并制定详细的实施计划".into(),
mode: "plan".into(),
},
QuickPhrase {
id: "default-pursue-goal".into(),
title: "持续完成这个目标".into(),
content: "持续推进这个目标,直到真正完成".into(),
mode: "goal".into(),
},
]
}

#[cfg(desktop)]
Expand Down Expand Up @@ -441,6 +475,7 @@ impl Default for AppPreferences {
browser_download_directory: None,
browser_ask_before_download: false,
appshots_play_sound: true,
quick_phrases: default_quick_phrases(),
}
}
}
Expand All @@ -463,6 +498,7 @@ struct AppPreferencesPatch {
browser_download_directory: Option<String>,
browser_ask_before_download: Option<bool>,
appshots_play_sound: Option<bool>,
quick_phrases: Option<Vec<QuickPhrase>>,
}

#[cfg(desktop)]
Expand Down Expand Up @@ -871,6 +907,9 @@ fn update_app_preferences(
if let Some(value) = patch.appshots_play_sound {
preferences.appshots_play_sound = value;
}
if let Some(value) = patch.quick_phrases {
preferences.quick_phrases = value;
}
write_app_preferences_impl(&app, &preferences)?;
Ok(preferences)
}
Expand All @@ -893,6 +932,7 @@ struct AppPreferences {
browser_download_directory: Option<String>,
browser_ask_before_download: bool,
appshots_play_sound: bool,
quick_phrases: Vec<QuickPhrase>,
}

#[cfg(not(desktop))]
Expand All @@ -913,6 +953,7 @@ struct AppPreferencesPatch {
browser_download_directory: Option<String>,
browser_ask_before_download: Option<bool>,
appshots_play_sound: Option<bool>,
quick_phrases: Option<Vec<QuickPhrase>>,
}

#[cfg(not(desktop))]
Expand All @@ -933,6 +974,7 @@ fn get_app_preferences(_app: tauri::AppHandle) -> Result<AppPreferences, String>
browser_download_directory: None,
browser_ask_before_download: false,
appshots_play_sound: true,
quick_phrases: default_quick_phrases(),
})
}

Expand Down Expand Up @@ -969,6 +1011,7 @@ fn update_app_preferences(
.and_then(normalized_non_empty),
browser_ask_before_download: patch.browser_ask_before_download.unwrap_or(false),
appshots_play_sound: patch.appshots_play_sound.unwrap_or(true),
quick_phrases: patch.quick_phrases.unwrap_or_else(default_quick_phrases),
})
}

Expand Down
11 changes: 11 additions & 0 deletions wework/src/components/chat/composer/CompactChatComposer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@ import { ComposerTextarea, type ComposerSubmitOptions } from './ComposerTextarea
import { ComposerModePill, GoalDraftPill } from './GoalDraftPill'
import { useAutoResizeTextarea } from './useAutoResizeTextarea'
import { debugComposerEvent, textMetrics } from './composerDebug'
import { QuickPhraseMenu } from './QuickPhraseMenu'
import type { QuickPhrase } from '@/tauri/appPreferences'

interface CompactChatComposerProps {
value: string
Expand Down Expand Up @@ -114,6 +116,14 @@ export function CompactChatComposer({
onRemoveAttachment(attachment.id)
window.requestAnimationFrame(() => textareaRef.current?.focus())
}
const handleQuickPhraseSelect = (phrase: QuickPhrase) => {
onClearPlanMode?.()
onCancelGoalDraft?.()
if (phrase.mode === 'plan') onSetPlanMode?.()
if (phrase.mode === 'goal') onSetGoal?.()
onChange(value ? `${value}\n${phrase.content}` : phrase.content)
window.requestAnimationFrame(() => textareaRef.current?.focus())
}

const handleImageChange = (event: ChangeEvent<HTMLInputElement>) => {
const files = event.target.files
Expand Down Expand Up @@ -220,6 +230,7 @@ export function CompactChatComposer({
>
<Plus className="h-6 w-6" />
</button>
<QuickPhraseMenu compact disabled={disabled} onSelect={handleQuickPhraseSelect} />
<div
data-testid="compact-input-pill"
className={[
Expand Down
5 changes: 5 additions & 0 deletions wework/src/components/chat/composer/ComposerToolbar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ import { AddContextMenu } from './AddContextMenu'
import { ComposerModePill, GoalDraftPill } from './GoalDraftPill'
import { ContextUsageIndicator } from './ContextUsageIndicator'
import { ModelSelector } from './ModelSelector'
import { QuickPhraseMenu } from './QuickPhraseMenu'
import type { QuickPhrase } from '@/tauri/appPreferences'

interface ComposerToolbarProps {
canSend: boolean
Expand All @@ -29,6 +31,7 @@ interface ComposerToolbarProps {
onCancelGoalDraft?: () => void
isStreaming?: boolean
onPause?: () => void
onQuickPhraseSelect: (phrase: QuickPhrase) => void
}

export function ComposerToolbar({
Expand All @@ -54,6 +57,7 @@ export function ComposerToolbar({
onCancelGoalDraft,
isStreaming = false,
onPause,
onQuickPhraseSelect,
}: ComposerToolbarProps) {
const { t } = useTranslation('common')

Expand All @@ -66,6 +70,7 @@ export function ComposerToolbar({
onSetPlanMode={planModeActive ? undefined : onSetPlanMode}
onSetGoal={onSetGoal}
/>
<QuickPhraseMenu disabled={disabled} onSelect={onQuickPhraseSelect} />
{goalDraftActive ? (
<GoalDraftPill onCancel={onCancelGoalDraft} />
) : planModeActive ? (
Expand Down
10 changes: 10 additions & 0 deletions wework/src/components/chat/composer/ProjectChatComposer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import { ComposerTextarea, type ComposerSubmitOptions } from './ComposerTextarea
import { ProjectWorkBar } from './ProjectWorkBar'
import { useAutoResizeTextarea } from './useAutoResizeTextarea'
import { debugComposerEvent, textMetrics } from './composerDebug'
import type { QuickPhrase } from '@/tauri/appPreferences'

interface ProjectChatComposerProps {
value: string
Expand Down Expand Up @@ -215,6 +216,14 @@ export function ProjectChatComposer({
onRemoveAttachment(attachment.id)
window.requestAnimationFrame(() => textareaRef.current?.focus())
}
const handleQuickPhraseSelect = (phrase: QuickPhrase) => {
onClearPlanMode?.()
onCancelGoalDraft?.()
if (phrase.mode === 'plan') onSetPlanMode?.()
if (phrase.mode === 'goal') onSetGoal?.()
onChange(value ? `${value}\n${phrase.content}` : phrase.content)
window.requestAnimationFrame(() => textareaRef.current?.focus())
}

useEffect(() => {
disabledRef.current = disabled
Expand Down Expand Up @@ -377,6 +386,7 @@ export function ProjectChatComposer({
onCancelGoalDraft={onCancelGoalDraft}
isStreaming={isStreaming}
onPause={onPause}
onQuickPhraseSelect={handleQuickPhraseSelect}
/>
</form>
</div>
Expand Down
37 changes: 37 additions & 0 deletions wework/src/components/chat/composer/QuickPhraseMenu.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import { fireEvent, render, screen } from '@testing-library/react'
import { describe, expect, test, vi } from 'vitest'
import { QuickPhraseMenu } from './QuickPhraseMenu'

vi.mock('@/hooks/useQuickPhrases', () => ({
useQuickPhrases: () => [
{ id: 'summary', title: '总结进展', content: '总结当前进展', mode: 'normal' },
{ id: 'plan', title: '制定计划', content: '制定实施计划', mode: 'plan' },
],
}))

vi.mock('@/lib/navigation', () => ({ navigateTo: vi.fn() }))

describe('QuickPhraseMenu', () => {
test('selects a phrase with the mouse', () => {
const onSelect = vi.fn()
render(<QuickPhraseMenu onSelect={onSelect} />)

fireEvent.click(screen.getByTestId('quick-phrase-button'))
fireEvent.click(screen.getByTestId('quick-phrase-option-plan'))

expect(onSelect).toHaveBeenCalledWith(expect.objectContaining({ id: 'plan' }))
expect(screen.queryByTestId('quick-phrase-menu')).not.toBeInTheDocument()
})

test('filters and selects a phrase with the keyboard', () => {
const onSelect = vi.fn()
render(<QuickPhraseMenu onSelect={onSelect} />)

fireEvent.click(screen.getByTestId('quick-phrase-button'))
const search = screen.getByTestId('quick-phrase-search-input')
fireEvent.change(search, { target: { value: '计划' } })
fireEvent.keyDown(search, { key: 'Enter' })

expect(onSelect).toHaveBeenCalledWith(expect.objectContaining({ id: 'plan' }))
})
})
Loading
Loading