feat: Enhance keyboard modifier support for macOS

This commit is contained in:
vegu-ai-tools
2026-04-16 00:13:44 +03:00
parent e2e0162b55
commit 29f9036033
27 changed files with 129 additions and 67 deletions

View File

@@ -34,6 +34,7 @@
- "Narrate Progress: Fixed narration sometimes generating screenplay-style dialogue entries instead of continuous narrative prose."
- "Advance Time: Fixed toolbar time advancement being completely non-functional due to a missing websocket handler. Also fixed invalid ISO 8601 duration strings for the 1 week and 2 weeks options."
- "Node Graph: Fixed `data/MakeDict` and `data/MakeList` leaking their initial value as a shared mutable reference across graph executions. Downstream in-place mutations (e.g., writing into a dict fed into SetState) would persist on the node and appear on the next run as stale data. Both nodes now deep-copy their configured default before emitting it."
- "macOS Modifiers: Ctrl+click affordances throughout the UI now also accept Cmd (Meta) on macOS, where the OS reserves Ctrl+click for the system context menu. Tooltips, hints, and docs label the modifier as Cmd on Mac and Ctrl elsewhere. Affects regenerate, scene tools (narrator/director/actor/creative), world state manager, contextual generate, message asset image, autocomplete (Ctrl+Enter), and history navigation. (#261)"
improvements:
- "System Prompt Override Indicators: The system prompt override list now shows a pencil icon next to entries that have an active override, making it easy to see which prompts have been customized."
- "Search Strictness: The distance_mod embedding preset value is now a float (was int) with a range of 0.12.0, allowing both tighter and looser similarity matching. A 'Search Strictness' slider is now available in the Context Database UI, letting users tune search sensitivity on the fly. Changes persist to the active embedding preset immediately."

View File

@@ -5,14 +5,14 @@
<!--- --8<-- [start:autocomplete] -->
!!! tip "Autocomplete using AI"
**:material-auto-fix: Autocomplete** - press `ctrl+Enter` while the cursor is in the input field to use AI to autocomplete the current content, you
**:material-auto-fix: Autocomplete** - press `ctrl+Enter` (or `cmd+Enter` on macOS) while the cursor is in the input field to use AI to autocomplete the current content, you
<!--- --8<-- [end:autocomplete] -->
<!--- --8<-- [start:generate_and_autocomplete] -->
!!! tip "Generate using AI"
**:material-auto-fix: Generate** - press the **generate** button on top of the input field to let the AI generate the content based on the existing details.
**:material-auto-fix: Autocomplete** - press `ctrl+Enter` while the cursor is in the input field to use AI to autocomplete the current content, you
**:material-auto-fix: Autocomplete** - press `ctrl+Enter` (or `cmd+Enter` on macOS) while the cursor is in the input field to use AI to autocomplete the current content, you
<!--- --8<-- [end:generate_and_autocomplete] -->
<!--- --8<-- [start:generation_templates_and_settings] -->

View File

@@ -38,8 +38,8 @@ If your voice agent is disabled - indicated by the grey dot next to the agent -
![Agent disabled](/talemate/img/0.26.0/agent-disabled.png) ![Agent enabled](/talemate/img/0.26.0/agent-enabled.png)
!!! note "Ctrl click to toggle agent"
You can use Ctrl click to toggle the agent on and off.
!!! note "Ctrl/Cmd click to toggle agent"
You can use Ctrl click (or Cmd click on macOS) to toggle the agent on and off.
## Voice Library Management

View File

@@ -1,5 +1,8 @@
# Scene Tools
!!! note "Mac users"
Throughout this guide, hold **Cmd** wherever **Ctrl** is mentioned. macOS reserves `Ctrl`+click for the system context menu, so Talemate also accepts the Command key as the primary modifier on every click and shortcut.
## Agent Activity Bar
![Agent activity bar showing active agents](/talemate/img/0.35.0/agent-activity-bar.png)

View File

@@ -106,6 +106,7 @@
<script>
import AgentModal from './AgentModal.vue';
import { isPrimaryModifier } from '@/utils/keyboardModifiers';
import AgentMessages from './AgentMessages.vue';
export default {
@@ -192,7 +193,7 @@ export default {
*/
listItemClicked(event, agent, index) {
// If the user is holding Ctrl (Windows/Linux) or Cmd (macOS)
if (event.ctrlKey || event.metaKey) {
if (isPrimaryModifier(event)) {
// Only toggle agents that explicitly allow it
if (agent.data && agent.data.has_toggle) {
agent.enabled = !agent.enabled;

View File

@@ -1,10 +1,12 @@
/**
* Mixin for components that use AssetView to display and navigate through a list of assets
*
*
* Requirements:
* - Component must implement 'assets' computed property (Array)
* - Component must implement 'getAssetSrc(assetId)' method (usually from VisualAssetsMixin)
*/
import { isPrimaryModifier } from '@/utils/keyboardModifiers';
export default {
data() {
return {
@@ -32,7 +34,7 @@ export default {
},
handleAssetClick(event, assetId, onMenuOpen) {
if (event.ctrlKey || event.metaKey) {
if (isPrimaryModifier(event)) {
event.stopPropagation();
event.preventDefault();
this.viewAsset(assetId);

View File

@@ -114,6 +114,7 @@
<script>
import { SceneTextParser } from '@/utils/sceneMessageRenderer';
import { insertNewlineAtCursor } from '@/utils/textAreaUtils';
import { isPrimaryModifier } from '@/utils/keyboardModifiers';
import MessageAssetImage from './MessageAssetImage.vue';
import MessageAssetMixin from './MessageAssetMixin.js';
export default {
@@ -282,7 +283,7 @@ export default {
// else -> submit
// shift -> newline
if (event.ctrlKey) {
if (isPrimaryModifier(event)) {
this.autocompleteEdit();
} else if (event.shiftKey) {
insertNewlineAtCursor(this.$refs.textarea, this.editing_text, (v) => this.editing_text = v);

View File

@@ -72,6 +72,7 @@
<script>
import { SceneTextParser } from '@/utils/sceneMessageRenderer';
import { insertNewlineAtCursor } from '@/utils/textAreaUtils';
import { isPrimaryModifier } from '@/utils/keyboardModifiers';
import MessageAssetImage from './MessageAssetImage.vue';
import MessageAssetMixin from './MessageAssetMixin.js';
@@ -181,7 +182,7 @@ export default {
// if ctrl -> autocomplete
// else -> submit
// shift -> newline
if (event.ctrlKey) {
if (isPrimaryModifier(event)) {
this.autocompleteEdit();
} else if (event.shiftKey) {
insertNewlineAtCursor(this.$refs.textarea, this.editing_text, (v) => this.editing_text = v);

View File

@@ -71,6 +71,7 @@
</template>
<script>
import { v4 as uuidv4 } from 'uuid';
import { isPrimaryModifier } from '@/utils/keyboardModifiers';
// In-memory (non-persistent) per-uid selection memory, capped to 100 entries.
// Uses Map insertion order as an LRU: re-setting a key moves it to the end.
@@ -260,8 +261,8 @@ export default {
this.dialog = true;
this.busy = false;
// if ctrl key is pressed, open with instructions
this.withInstructions = event.ctrlKey || this.requiresInstructions;
// if ctrl/cmd key is pressed, open with instructions
this.withInstructions = isPrimaryModifier(event) || this.requiresInstructions;
// if alt key is pressed, open with original
this.withOriginal = event.altKey && this.rewriteEnabled;

View File

@@ -38,6 +38,7 @@
<script>
import AssetView from './AssetView.vue';
import { isPrimaryModifier } from '@/utils/keyboardModifiers';
export default {
name: 'MessageAssetImage',
@@ -164,8 +165,8 @@ export default {
},
methods: {
handleClick(event) {
// Ctrl+click directly opens the image view
if (event.ctrlKey && this.imageSrc) {
// Ctrl/Cmd+click directly opens the image view
if (isPrimaryModifier(event) && this.imageSrc) {
this.openAssetView();
return;
}

View File

@@ -109,6 +109,7 @@
<script>
import { SceneTextParser } from '@/utils/sceneMessageRenderer';
import { insertNewlineAtCursor } from '@/utils/textAreaUtils';
import { isPrimaryModifier } from '@/utils/keyboardModifiers';
import MessageAssetImage from './MessageAssetImage.vue';
import MessageAssetMixin from './MessageAssetMixin.js';
export default {
@@ -225,7 +226,7 @@ export default {
// else -> submit
// shift -> newline
if (event.ctrlKey) {
if (isPrimaryModifier(event)) {
this.autocompleteEdit();
} else if (event.shiftKey) {
insertNewlineAtCursor(this.$refs.textarea, this.editing_text, (v) => this.editing_text = v);

View File

@@ -221,7 +221,7 @@
:style="propertyEditorStyle"
@keydown.capture="onPropertyEditorKeydown"
></Codemirror>
<span class="text-caption text-muted">(Ctrl+Enter to submit changes)</span>
<span class="text-caption text-muted">({{ primaryModifierLabel }}+Enter to submit changes)</span>
</v-card-text>
<v-card-actions v-if="propertyEditorType === 'color'">
<v-spacer></v-spacer>
@@ -311,6 +311,7 @@
<script>
import { LGraphCanvas, LiteGraph } from 'litegraph.js';
import { initializeGraphFromJSON, normalizeHexColor } from '@/utils/litegraphUtils'
import { isPrimaryModifier, primaryModifierLabel } from '@/utils/keyboardModifiers';
import { convertGraphToJSON, convertSelectedGraphToJSON } from '@/utils/exportGraph.js'
//import '@/utils/litegraphSearchBox'
import { Codemirror } from 'vue-codemirror'
@@ -367,6 +368,7 @@ export default {
data() {
return {
primaryModifierLabel,
propertyEditor: false,
propertyEditorType: 'text',
propertyEditorValue: '',
@@ -607,7 +609,7 @@ export default {
submitPropertyEditor(ev) {
if(ev && !(ev.ctrlKey || ev.metaKey)) {
if(ev && !isPrimaryModifier(ev)) {
return;
}
if(ev) {
@@ -638,7 +640,7 @@ export default {
},
onPropertyEditorKeydown(ev) {
if ((ev.ctrlKey || ev.metaKey) && ev.key === 'Enter') {
if (isPrimaryModifier(ev) && ev.key === 'Enter') {
ev.preventDefault();
ev.stopPropagation();
this.submitPropertyEditor(ev);

View File

@@ -18,10 +18,11 @@
v-model="input"
ref="input_multiline"
@keydown.enter.ctrl="proceed"
@keydown.enter.meta="proceed"
:placeholder="placeholder"
:label="title"
:rules="[rules.required]"
messages="Press Ctrl+Enter to submit"
:messages="`Press ${primaryModifierLabel}+Enter to submit`"
></v-textarea>
</v-col>
</v-row>
@@ -31,10 +32,11 @@
v-model="input"
ref="input_text"
@keydown.enter.ctrl="proceed"
@keydown.enter.meta="proceed"
:label="title"
:placeholder="placeholder"
:rules="[rules.required]"
messages="Press Ctrl+Enter to submit"
:messages="`Press ${primaryModifierLabel}+Enter to submit`"
></v-text-field>
</v-col>
</v-row>
@@ -54,6 +56,7 @@
</template>
<script>
import { primaryModifierLabel } from '@/utils/keyboardModifiers';
export default {
name: "RequestInput",
@@ -80,6 +83,7 @@ export default {
},
data() {
return {
primaryModifierLabel,
open: false,
valid: false,
extra_params: {},

View File

@@ -26,7 +26,7 @@
@click="handleViewImage"
>
<v-list-item-title>View Image</v-list-item-title>
<v-list-item-subtitle>Ctrl+click</v-list-item-subtitle>
<v-list-item-subtitle>{{ primaryModifierLabel }}+click</v-list-item-subtitle>
</v-list-item>
<v-divider></v-divider>
<v-list-item
@@ -375,6 +375,7 @@ import VisualReferenceCarousel from './VisualReferenceCarousel.vue';
import ConfirmActionPrompt from './ConfirmActionPrompt.vue';
import VisualAssetsMixin from './VisualAssetsMixin.js';
import { isVisualAgentReady } from '../constants/visual.js';
import { primaryModifierLabel } from '@/utils/keyboardModifiers';
const MESSAGE_FLAGS = {
NONE: 0,
@@ -503,6 +504,7 @@ export default {
emits: ['cancel-audio-queue'],
data() {
return {
primaryModifierLabel,
messages: [],
selectedForkMessageId: null,
defaultColors: {

View File

@@ -59,7 +59,7 @@
<RequestInput
ref="requestDirectedRegenerate"
title="Directed regenerate"
instructions="Provide instructions for regeneration. Ctrl+Enter submits."
:instructions="`Provide instructions for regeneration. ${primaryModifierLabel}+Enter submits.`"
icon="mdi-refresh"
inputType="multiline"
@continue="directedRegenerateContinue"
@@ -107,7 +107,7 @@
</v-tooltip>
<v-tooltip :disabled="appBusy || !appReady" location="top"
:text="'Redo most recent AI message.\n[Ctrl: Provide instructions, +Alt: Rewrite]'"
:text="`Redo most recent AI message.\n[${primaryModifierLabel}: Provide instructions, +Alt: Rewrite]`"
class="pre-wrap"
max-width="300px">
<template v-slot:activator="{ props }">
@@ -119,7 +119,7 @@
</v-tooltip>
<v-tooltip :disabled="appBusy || !appReady" location="top"
:text="'Redo most recent AI message (Nuke Option - use this to attempt to break out of repetition) \n[Ctrl: Provide instructions, +Alt: Rewrite]'"
:text="`Redo most recent AI message (Nuke Option - use this to attempt to break out of repetition) \n[${primaryModifierLabel}: Provide instructions, +Alt: Rewrite]`"
class="pre-wrap"
max-width="300px">
<template v-slot:activator="{ props }">
@@ -223,6 +223,7 @@ import SceneToolsSettings from './SceneToolsSettings.vue';
import SceneToolsSave from './SceneToolsSave.vue';
import SceneToolsTime from './SceneToolsTime.vue';
import RequestInput from './RequestInput.vue';
import { isPrimaryModifier, primaryModifierLabel } from '@/utils/keyboardModifiers';
export default {
name: 'SceneTools',
@@ -284,6 +285,7 @@ export default {
},
data() {
return {
primaryModifierLabel,
commandActive: false,
commandName: null,
autoSave: true,
@@ -368,9 +370,9 @@ export default {
},
regenerate(event) {
// if ctrl is pressed use directed regenerate
let withDirection = event.ctrlKey;
let method = event.altKey || event.metaKey ? "edit" : "replace";
// primary modifier (Ctrl/Cmd) opens the directed regenerate dialog
let withDirection = isPrimaryModifier(event);
let method = event.altKey ? "edit" : "replace";
const nuke_repetition = 0.0;
if (withDirection) {
@@ -390,9 +392,9 @@ export default {
},
regenerateNuke(event) {
// if ctrl is pressed use directed regenerate
let withDirection = event.ctrlKey;
let method = event.altKey || event.metaKey ? "edit" : "replace";
// primary modifier (Ctrl/Cmd) opens the directed regenerate dialog
let withDirection = isPrimaryModifier(event);
let method = event.altKey ? "edit" : "replace";
const nuke_repetition = 0.5;
if (withDirection) {

View File

@@ -10,12 +10,12 @@
<v-list-item v-for="npcName in npcCharacters" :key="npcName"
@click="(ev) => { actionGenerateActingAction(ev, null, {character: npcName}) }" prepend-icon="mdi-comment-account-outline">
<v-list-item-title>Actor Action ({{ npcName }})</v-list-item-title>
<v-list-item-subtitle>Generate Actor Action <v-chip variant="text" color="highlight5" class="ml-1" size="x-small">Ctrl: Provide direction</v-chip></v-list-item-subtitle>
<v-list-item-subtitle>Generate Actor Action <v-chip variant="text" color="highlight5" class="ml-1" size="x-small">{{ primaryModifierLabel }}: Provide direction</v-chip></v-list-item-subtitle>
</v-list-item>
<v-list-item @click="actionGenerateActingAction" prepend-icon="mdi-comment-text-outline">
<v-list-item-title>Actor Action</v-list-item-title>
<v-list-item-subtitle>Generate Actor Action <v-chip variant="text" color="highlight5" class="ml-1" size="x-small">Ctrl: Provide direction</v-chip></v-list-item-subtitle>
<v-list-item-subtitle>Generate Actor Action <v-chip variant="text" color="highlight5" class="ml-1" size="x-small">{{ primaryModifierLabel }}: Provide direction</v-chip></v-list-item-subtitle>
</v-list-item>
</v-list>
</v-menu>
@@ -29,6 +29,7 @@
<script>
import RequestInput from './RequestInput.vue';
import { isPrimaryModifier, primaryModifierLabel } from '@/utils/keyboardModifiers';
export default {
name: "SceneToolsActor",
@@ -42,6 +43,7 @@ export default {
inject: ['getWebsocket'],
data() {
return {
primaryModifierLabel,
}
},
methods: {
@@ -69,7 +71,7 @@ export default {
actionGenerateActingAction(ev, actingDirection="", params) {
if (ev.ctrlKey) {
if (isPrimaryModifier(ev)) {
this.requestDirection({action: 'GenerateActingAction', ...params});
return;
}

View File

@@ -15,7 +15,7 @@
<template v-slot:prepend>
<v-icon color="secondary">mdi-exit-run</v-icon>
</template>
<v-list-item-title>Take out of scene: {{ character }}<v-chip variant="text" color="info" class="ml-1" size="x-small">Ctrl: no narration</v-chip></v-list-item-title>
<v-list-item-title>Take out of scene: {{ character }}<v-chip variant="text" color="info" class="ml-1" size="x-small">{{ primaryModifierLabel }}: no narration</v-chip></v-list-item-title>
<v-list-item-subtitle>Make {{ character }} a passive character.</v-list-item-subtitle>
</v-list-item>
@@ -25,7 +25,7 @@
<template v-slot:prepend>
<v-icon color="secondary">mdi-human-greeting</v-icon>
</template>
<v-list-item-title>Call into scene: {{ character }}<v-chip variant="text" color="info" class="ml-1" size="x-small">Ctrl: no narration</v-chip></v-list-item-title>
<v-list-item-title>Call into scene: {{ character }}<v-chip variant="text" color="info" class="ml-1" size="x-small">{{ primaryModifierLabel }}: no narration</v-chip></v-list-item-title>
<v-list-item-subtitle>Make {{ character }} an active character.</v-list-item-subtitle>
</v-list-item>
@@ -44,7 +44,7 @@
<template v-slot:prepend>
<v-icon color="warning">mdi-human-greeting</v-icon>
</template>
<v-list-item-title>Introduce {{ character }}<v-chip variant="text" color="highlight5" class="ml-1" size="x-small">Ctrl: Advanced</v-chip></v-list-item-title>
<v-list-item-title>Introduce {{ character }}<v-chip variant="text" color="highlight5" class="ml-1" size="x-small">{{ primaryModifierLabel }}: Advanced</v-chip></v-list-item-title>
<v-list-item-subtitle>Make {{ character }} an active character.</v-list-item-subtitle>
</v-list-item>
@@ -134,6 +134,7 @@
<script>
import WorldStateManagerTemplateApplicator from './WorldStateManagerTemplateApplicator.vue';
import { isPrimaryModifier, primaryModifierLabel } from '@/utils/keyboardModifiers';
export default {
name: 'SceneToolsCreative',
components: {
@@ -186,6 +187,7 @@ export default {
},
data() {
return {
primaryModifierLabel,
dialogIntroduceCharacter: false,
newIntroduction: null,
introduction: {
@@ -246,12 +248,12 @@ export default {
},
activateCharacter(ev, name) {
let modifyNoNarration = ev.ctrlKey;
let modifyNoNarration = isPrimaryModifier(ev);
this.getWebsocket().send(JSON.stringify({ type: 'director', action: 'activate_character', character_name: name, never_narrate: modifyNoNarration }));
},
deactivateCharacter(ev, name) {
let modifyNoNarration = ev.ctrlKey;
let modifyNoNarration = isPrimaryModifier(ev);
this.getWebsocket().send(JSON.stringify({ type: 'director', action: 'deactivate_character', character_name: name, never_narrate: modifyNoNarration }));
},
@@ -276,7 +278,7 @@ export default {
},
introduceCharacter(ev, name) {
let advanced = ev.ctrlKey;
let advanced = isPrimaryModifier(ev);
let payload = {};
if(typeof name === 'string' && !advanced) {

View File

@@ -13,7 +13,7 @@
@click="actionRequestDynamicChoices"
prepend-icon="mdi-tournament"
>
<v-list-item-title>Generate dynamic actions<v-chip variant="text" color="highlight5" class="ml-1" size="x-small">Ctrl: Provide direction</v-chip></v-list-item-title>
<v-list-item-title>Generate dynamic actions<v-chip variant="text" color="highlight5" class="ml-1" size="x-small">{{ primaryModifierLabel }}: Provide direction</v-chip></v-list-item-title>
<v-list-item-subtitle>{{ getActAsCharacterName() }}</v-list-item-subtitle>
</v-list-item>
<!-- Trigger scene direction turn -->
@@ -23,7 +23,7 @@
prepend-icon="mdi-movie-play"
:disabled="!sceneDirectionEnabled"
>
<v-list-item-title>Scene direction turn<v-chip variant="text" color="highlight5" class="ml-1" size="x-small">Ctrl: Provide direction</v-chip></v-list-item-title>
<v-list-item-title>Scene direction turn<v-chip variant="text" color="highlight5" class="ml-1" size="x-small">{{ primaryModifierLabel }}: Provide direction</v-chip></v-list-item-title>
<v-list-item-subtitle>Manually trigger a scene direction turn</v-list-item-subtitle>
</v-list-item>
<!-- Generate long progress -->
@@ -156,6 +156,7 @@
<script>
import RequestInput from './RequestInput.vue';
import { isPrimaryModifier, primaryModifierLabel } from '@/utils/keyboardModifiers';
export default {
name: "SceneToolsDirector",
@@ -171,6 +172,7 @@ export default {
inject: ['getWebsocket', 'getActAsCharacterName', 'openDirectorConsole', 'openAgentSettings'],
data() {
return {
primaryModifierLabel,
scenePlanDialog: false,
scenePlanInstructions: '',
scenePlanBeats: 8,
@@ -222,7 +224,7 @@ export default {
actionRequestDynamicChoices(ev, instructions="") {
if (ev.ctrlKey) {
if (isPrimaryModifier(ev)) {
this.requestDirection({action: 'RequestDynamicChoices'});
return;
}
@@ -245,7 +247,7 @@ export default {
actionSceneDirectionTurn(ev, instructions="") {
if (ev.ctrlKey) {
if (isPrimaryModifier(ev)) {
this.requestDirection({action: 'SceneDirectionTurn'});
return;
}

View File

@@ -16,7 +16,7 @@
@click="actionProgress"
prepend-icon="mdi-script-text-play"
>
<v-list-item-title>Progress Story <v-chip variant="text" color="highlight5" class="ml-1" size="x-small">Ctrl: Provide direction</v-chip></v-list-item-title>
<v-list-item-title>Progress Story <v-chip variant="text" color="highlight5" class="ml-1" size="x-small">{{ primaryModifierLabel }}: Provide direction</v-chip></v-list-item-title>
<v-btn @click.stop="(e) => { actionProgress(e, PROGRESS_SHORTCUT_MINOR) }" variant="tonal" color="highlight2" class="mr-1" size="x-small" :prepend-icon="'mdi-dice-multiple'">Minor</v-btn>
<v-btn @click.stop="(e) => { actionProgress(e, PROGRESS_SHORTCUT_MAJOR) }" variant="tonal" color="highlight2" class="mr-1" size="x-small" :prepend-icon="'mdi-dice-multiple'">Major</v-btn>
<v-btn @click.stop="(e) => { actionProgress(e, PROGRESS_SHORTCUT_CURVEBALL) }" variant="tonal" color="highlight2" size="x-small" :prepend-icon="'mdi-dice-multiple'">Curveball</v-btn>
@@ -28,7 +28,7 @@
@click="actionNarrateEnvironment"
prepend-icon="mdi-waves"
>
<v-list-item-title>Narrate Environment <v-chip variant="text" color="highlight5" class="ml-1" size="x-small">Ctrl: Provide direction</v-chip></v-list-item-title>
<v-list-item-title>Narrate Environment <v-chip variant="text" color="highlight5" class="ml-1" size="x-small">{{ primaryModifierLabel }}: Provide direction</v-chip></v-list-item-title>
</v-list-item>
<!-- Look at Scene -->
@@ -37,7 +37,7 @@
@click="actionLookAtScene"
prepend-icon="mdi-image-filter-hdr"
>
<v-list-item-title>Look at Scene <v-chip variant="text" color="highlight5" class="ml-1" size="x-small">Ctrl: Provide direction</v-chip></v-list-item-title>
<v-list-item-title>Look at Scene <v-chip variant="text" color="highlight5" class="ml-1" size="x-small">{{ primaryModifierLabel }}: Provide direction</v-chip></v-list-item-title>
</v-list-item>
<!-- Look at NPCs -->
@@ -47,7 +47,7 @@
@click="(ev) => { actionLookAtCharacter(ev, null, {character: npc_name}) }"
prepend-icon="mdi-account-eye"
>
<v-list-item-title>Look at {{ npc_name }} <v-chip variant="text" color="highlight5" class="ml-1" size="x-small">Ctrl: Provide direction</v-chip></v-list-item-title>
<v-list-item-title>Look at {{ npc_name }} <v-chip variant="text" color="highlight5" class="ml-1" size="x-small">{{ primaryModifierLabel }}: Provide direction</v-chip></v-list-item-title>
</v-list-item>
<!-- Query -->
@@ -76,6 +76,7 @@
<script>
import RequestInput from './RequestInput.vue';
import { isPrimaryModifier, primaryModifierLabel } from '@/utils/keyboardModifiers';
const PROGRESS_SHORTCUT_MINOR = "Introduce a minor change to the story.";
const PROGRESS_SHORTCUT_MAJOR = "Introduce a major shift in the story.";
@@ -96,6 +97,7 @@ export default {
PROGRESS_SHORTCUT_MINOR,
PROGRESS_SHORTCUT_MAJOR,
PROGRESS_SHORTCUT_CURVEBALL,
primaryModifierLabel,
}
},
methods: {
@@ -146,7 +148,7 @@ export default {
actionProgress(ev, narrativeDirection="") {
if (ev.ctrlKey) {
if (isPrimaryModifier(ev)) {
this.requestDirection({action: 'Progress', input: narrativeDirection});
return;
}
@@ -168,7 +170,7 @@ export default {
actionNarrateEnvironment(ev, narrativeDirection="") {
if (ev.ctrlKey) {
if (isPrimaryModifier(ev)) {
this.requestDirection({action: 'NarrateEnvironment'});
return;
}
@@ -191,7 +193,7 @@ export default {
actionLookAtScene(ev, narrativeDirection="") {
if (ev.ctrlKey) {
if (isPrimaryModifier(ev)) {
this.requestDirection({action: 'LookAtScene'});
return;
}
@@ -213,8 +215,8 @@ export default {
*/
actionLookAtCharacter(ev, narrativeDirection="", params) {
if (ev.ctrlKey) {
if (isPrimaryModifier(ev)) {
this.requestDirection({action: 'LookAtCharacter', ...params});
return;
}

View File

@@ -91,6 +91,7 @@
</template>
<script>
import { VIS_TYPE_OPTIONS } from '../constants/visual';
import { isPrimaryModifier } from '@/utils/keyboardModifiers';
export default {
name: 'SceneToolsVisual',
props: {
@@ -183,7 +184,7 @@ export default {
return null;
},
handleVisualize(character_name, event, vis_type = 'CHARACTER_CARD') {
const ctrlPressed = event.ctrlKey || event.metaKey;
const ctrlPressed = isPrimaryModifier(event);
const altPressed = event.altKey;
if (ctrlPressed) {

View File

@@ -294,7 +294,9 @@
ref="messageInput"
@keydown.enter.prevent="sendMessage"
@keydown.ctrl.up.prevent="onHistoryUp"
@keydown.meta.up.prevent="onHistoryUp"
@keydown.ctrl.down.prevent="onHistoryDown"
@keydown.meta.down.prevent="onHistoryDown"
@keydown.tab.prevent="cycleActAs"
:hint="messageInputLongHint()"
:disabled="busy || !ready || uxInteractionActive || isInputDisabled()"
@@ -386,6 +388,7 @@
<script>
import AIClient from './AIClient.vue';
import { isPrimaryModifier, primaryModifierLabel } from '@/utils/keyboardModifiers';
import AIAgent from './AIAgent.vue';
import AgentActivityBar from './AgentActivityBar.vue';
import LoadScene from './LoadScene.vue';
@@ -1318,8 +1321,8 @@ export default {
sendMessage(event) {
// if ctrl+enter is pressed, request autocomplete
if (event.ctrlKey && event.key === 'Enter') {
// if ctrl/cmd+enter is pressed, request autocomplete
if (isPrimaryModifier(event) && event.key === 'Enter') {
return this.autocomplete();
}
@@ -1525,7 +1528,7 @@ export default {
},
autocompleteInfoMessage(active) {
return active ? 'Generating ...' : "Ctrl+Enter to autocomplete";
return active ? 'Generating ...' : `${primaryModifierLabel}+Enter to autocomplete`;
},
requestAppConfig() {
@@ -1803,7 +1806,7 @@ export default {
},
messageInputLongHint() {
const DIALOG_HINT = "Ctrl+Enter to autocomplete, Shift+Enter for newline, Ctrl+Up/Down for history, Tab to act as another character. Start messages with '@' to do an action. (e.g., '@look at the door')";
const DIALOG_HINT = `${primaryModifierLabel}+Enter to autocomplete, Shift+Enter for newline, ${primaryModifierLabel}+Up/Down for history, Tab to act as another character. Start messages with '@' to do an action. (e.g., '@look at the door')`;
if(this.waitingForInput) {
if(this.inputRequestInfo.reason === "talk") {

View File

@@ -235,6 +235,7 @@
<script>
import { VIS_TYPE_OPTIONS } from '../constants/visual.js';
import { isPrimaryModifier } from '@/utils/keyboardModifiers';
import VisualReferenceImages from './VisualReferenceImages.vue';
import CoverBBoxEditor from './CoverBBoxEditor.vue';
export default {
@@ -392,7 +393,7 @@ export default {
},
handleAnalyzeClick(event) {
// If Ctrl/Cmd is pressed, open dialog; otherwise quick analyze
if (event.ctrlKey || event.metaKey) {
if (isPrimaryModifier(event)) {
event.preventDefault();
event.stopPropagation();
this.showAnalyzeDialog = true;

View File

@@ -46,7 +46,7 @@
:loading="analyzing"
>
<v-tooltip activator="parent" location="top">
Analyze the image using AI. (Ctrl: set instructions)
Analyze the image using AI. ({{ primaryModifierLabel }}: set instructions)
</v-tooltip>
Analyze
</v-btn>
@@ -158,6 +158,7 @@ import ConfirmActionPrompt from './ConfirmActionPrompt.vue';
import VisualLibraryUpload from './VisualLibraryUpload.vue';
import VisualAssetsTree from './VisualAssetsTree.vue';
import { debounce } from 'lodash';
import { isPrimaryModifier, primaryModifierLabel } from '@/utils/keyboardModifiers';
export default {
name: 'VisualLibraryScene',
@@ -175,6 +176,7 @@ export default {
},
data() {
return {
primaryModifierLabel,
base64ById: {},
assetSearchInput: '',
assetSearch: '',
@@ -399,7 +401,7 @@ export default {
return;
}
// If Ctrl/Cmd is pressed, open dialog; otherwise quick analyze
if (event.ctrlKey || event.metaKey) {
if (isPrimaryModifier(event)) {
event.preventDefault();
event.stopPropagation();
this.showAnalyzeDialog = true;

View File

@@ -115,10 +115,10 @@
<!-- GENERATE CHANGE SUGGESTIONS -->
<div>
<v-list-item>
<v-tooltip max-width="300" :text="`Generate change suggestions for ${character.name}. This will provide a list of suggestions for changes to the character, based on the progression of the story thus far. [Ctrl: Provide instructions]`">
<v-tooltip max-width="300" :text="`Generate change suggestions for ${character.name}. This will provide a list of suggestions for changes to the character, based on the progression of the story thus far. [${primaryModifierLabel}: Provide instructions]`">
<template v-slot:activator="{ props }">
<v-btn
@click.stop="(event) => { suggestChanges(character.name, event.ctrlKey)}"
<v-btn
@click.stop="(event) => { suggestChanges(character.name, isPrimaryModifier(event))}"
v-bind="props"
variant="tonal"
:disabled="appBusy || !appReady"
@@ -350,6 +350,7 @@ import WorldStateManagerCharacterActor from './WorldStateManagerCharacterActor.v
import WorldStateManagerCharacterCreator from './WorldStateManagerCharacterCreator.vue';
import WorldStateManagerCharacterVisuals from './WorldStateManagerCharacterVisuals.vue';
import { MAX_CONTENT_WIDTH, FOLDER_NAME_MAX_LENGTH } from '@/constants';
import { isPrimaryModifier, primaryModifierLabel } from '@/utils/keyboardModifiers';
export default {
name: 'WorldStateManagerCharacter',
@@ -397,6 +398,7 @@ export default {
folderInput: '',
folderMenuOpen: false,
MAX_CONTENT_WIDTH,
primaryModifierLabel,
}
},
watch: {
@@ -447,6 +449,7 @@ export default {
},
},
methods: {
isPrimaryModifier,
reset() {
this.selected = null;
this.character = null;

View File

@@ -19,7 +19,7 @@
<span class="text-primary">{{ pendingDelete ? 'Deleting...' : 'Regenerating...' }}</span>
</div>
<div v-else>
<v-btn v-if="hasSourceEntries" :disabled="editing || locked" prepend-icon="mdi-refresh" color="primary" @click="(ev) => regenerateEntry(ev.ctrlKey)">Regenerate</v-btn>
<v-btn v-if="hasSourceEntries" :disabled="editing || locked" prepend-icon="mdi-refresh" color="primary" @click="(ev) => regenerateEntry(isPrimaryModifier(ev))">Regenerate</v-btn>
<v-btn v-if="hasSourceEntries" :disabled="editing || locked" color="primary" prepend-icon="mdi-magnify-expand" @click="toggleSourceEntries">{{ entry.source_entries ? 'Collapse' : 'Inspect' }}</v-btn>
<v-btn v-if="hasSourceEntries" :disabled="editing || locked" prepend-icon="mdi-clock-plus-outline" color="primary" @click="insertingTimePassage = true">Time Passage</v-btn>
<ConfirmActionInline
@@ -106,6 +106,7 @@ class HistoryEntry(pydantic.BaseModel):
*/
import { SceneTextParser } from '@/utils/sceneMessageRenderer';
import { isPrimaryModifier } from '@/utils/keyboardModifiers';
import ContextualGenerate from './ContextualGenerate.vue';
import ConfirmActionInline from './ConfirmActionInline.vue';
@@ -165,6 +166,7 @@ export default {
}
},
methods: {
isPrimaryModifier,
handleEnter(ev) {
if(!ev.shiftKey) {
ev.preventDefault();

View File

@@ -0,0 +1,19 @@
// Helpers for cross-platform keyboard modifier handling.
//
// On macOS, Ctrl+click is intercepted by the OS as a context-menu (right-click)
// gesture, so any "hold Ctrl while clicking" affordance is unreachable for Mac
// users. Treat the Cmd (Meta) key as the equivalent primary modifier so the
// same interactions work on every platform.
export const isMac = typeof navigator !== 'undefined'
&& /Mac|iPhone|iPad|iPod/i.test(navigator.platform || navigator.userAgent || '');
// True when the user is holding the platform's primary modifier (Ctrl on
// Windows/Linux, Cmd on macOS). Accept both on every platform so external
// keyboards keep working.
export function isPrimaryModifier(event) {
return !!event && (event.ctrlKey || event.metaKey);
}
// Human-readable label for the primary modifier, for use in tooltips.
export const primaryModifierLabel = isMac ? 'Cmd' : 'Ctrl';

View File

@@ -4,6 +4,7 @@ import { CommentNode } from './commentNode.js';
import { trackRecentNodes } from './recentNodes.js';
import { handleFitGroupToNodes, handleDuplicateGroup, handleVerticalSnapGroup, handleCreateGroupFromSelectedNodes } from './groupInteractions.js';
import { handleWatchNodeShortcut, handleSetStateNodeShortcut, handleStageNodeShortcut } from './graphConnectionUtil.js';
import { isPrimaryModifier } from './keyboardModifiers.js';
const UNRESOLVED = "<class 'talemate.game.engine.nodes.core.UNRESOLVED'>";
@@ -1605,7 +1606,7 @@ LGraphCanvas.prototype.processKey = function(e) {
var key_code = e.keyCode;
// Ctrl+C to copy
if (e.type == "keydown" && (e.ctrlKey || e.metaKey) && key_code == 67) {
if (e.type == "keydown" && isPrimaryModifier(e) && key_code == 67) {
if (this.selected_nodes) {
// Store the selected nodes for copy
var selected_list = [];
@@ -1672,7 +1673,7 @@ LGraphCanvas.prototype.processKey = function(e) {
block_default = true;
}
// Ctrl+V to paste
else if (e.type == "keydown" && (e.ctrlKey || e.metaKey) && key_code == 86) {
else if (e.type == "keydown" && isPrimaryModifier(e) && key_code == 86) {
if (LiteGraph._clipboard_data && LiteGraph._clipboard_data.nodes && LiteGraph._clipboard_data.nodes.length) {
this.graph.beforeChange();
@@ -1835,8 +1836,8 @@ LGraphCanvas.prototype.processMouseDown = function(e) {
if (isClickOnTitle) {
let handled = false;
// --- Ctrl+Click: Fit Group to Nodes ---
if (e.ctrlKey || e.metaKey) {
// --- Ctrl/Cmd+Click: Fit Group to Nodes ---
if (isPrimaryModifier(e)) {
handled = handleFitGroupToNodes(group, this);
}
// --- Shift+Click: Duplicate Group ---