תיעוד 77
ציור בממשק עם מוד
ציור חלוניות, פס מעל הפרומפט, כפתורים ושדות טקסט מתוך מוד של Claude Code, טיפול בלחיצות ובקלט, ושמירת מצב בין ציורים מחדש ובין סשנים.
מוד יכול לצייר ממשק משלו ב-Claude Code ולשנות חלקים מהממשק ש-Claude Code כבר מצייר. כל מקום שמוד יכול לצייר בו נקרא אתר ציור (render site), למשל חלונית, הפס מעל הפרומפט, או הספינר. Claude Code מפעיל את אירוע ui.render בכל פעם שהוא עומד לצייר אתר ציור, וההוק שלך לאירוע הזה מחזיר מה לצייר שם.
המפה הזו מראה איפה מוד יכול לצייר בסשן טרמינל:
בטרמינל צר יותר, החלונית יושבת מעל הפרומפט במקום לצד התמליל.
בנה את המוד הראשון לפני שמתחילים כאן. התחל בדוגמה המפורטת, שבונה חלונית עם שתי לשוניות ומונה, ואז קרא את הסעיף של כל חלק שאתה רוצה לשנות.
[!NOTE]
כדי לחפש
propאחד או מגבלה, ראה את ההפניה.
#בניית חלונית עם לשוניות
בסעיף זה בונים מוד שמוסיף פקודה /hello-tabs, והפקודה פותחת חלונית. חלונית היא סרגל צד לצד התמליל בטרמינל רחב במסך מלא, או אזור עם מסגרת מעל הפרומפט בכל מקרה אחר. החלונית הזו מציגה שתי לשוניות, וללשונית השנייה יש כפתור שמוסיף אחד למונה. הספירה עדיין שם אחרי שמפעילים מחדש את Claude Code.
המוד המוגמר נראה כך. ההקלטה פותחת את החלונית, עוברת ללשונית השנייה, לוחצת על הכפתור כמה פעמים, וחוזרת ללשונית הראשונה:
הלשוניות הן שני כפתורים בשורה. המוד עוקב אחרי איזו מהן פעילה ומצייר את התוכן של הלשונית הזו מתחת לשורה.
1. יצירת התוסף
מוד הוא תוסף עם מניפסט, קובץ hooks.json שמצביע על הקוד שלך, וקובץ הקוד. יצירת מוד מסבירה כל אחד מהם. צור ספרייה בשם hello-tabs עם הספריות .claude-plugin ו-hooks בתוכה, ואז שמור את שני הקבצים הראשונים.
שמור את המניפסט כ-hello-tabs/.claude-plugin/plugin.json:
{
"name": "hello-tabs",
"version": "0.1.0",
"description": "Opens a pane with two tabs and a counter",
"author": { "name": "Your Name" }
}תן שם לנקודת הכניסה ב-hello-tabs/hooks/hooks.json:
{
"modules": ["./register.js"]
}2. כתיבת הקוד
הרשימה הזו אומרת מה כל הוק עושה, לפי הסדר שבו הם מופיעים בקוד:
- מוסיף את הפקודה
/hello-tabs, וטוען את הספירה שסשן קודם שמר - פותח את החלונית כשמריצים את הפקודה הזו
- מצייר את תוכן החלונית: שורת הלשוניות וגוף הלשונית הפתוחה
שני משתנים ברמת המודול, tab ו-count, מחזיקים את מצב החלונית.
שמור את זה כ-hello-tabs/hooks/register.js:
// The pane's id, used to open the pane and to recognize it when drawing
const PANE = 'hello-tabs'
// What the pane shows: which tab is open, and the counter's value
let tab = 'one'
let count = 0
export function register(on) {
// Runs before your first prompt, and again after a reload
on('session.start', async ($, e, next) => {
await $.command.register({ name: 'hello-tabs', description: 'Open the hello-tabs pane' })
// Load the count an earlier session saved, if there is one
const saved = await $.store.get('count')
if (typeof saved === 'number') count = saved
return next(e)
})
// Runs when you type /hello-tabs
on('command.run', { command: 'hello-tabs' }, async ($) => {
// Open the pane, give it the keyboard, and let Esc close it
await $.ui.open({ id: PANE, title: 'Hello tabs', focus: true, closeOnEscape: true })
// Print nothing in the transcript
return {}
})
// Runs each time Claude Code draws a pane
on('ui.render', { component: 'Pane' }, async ($, e, next) => {
// Leave other mods' panes alone
if (e.requestId !== PANE) return next(e)
// Get the elements this app can draw
const { Box, Text, Button } = $.ui.resolve(e)
// Ask Claude Code to run this hook again
const redraw = () => $.ui.invalidate('ui.render')
// One tab: a button that switches to its tab when pressed
const tabButton = (name, label, hotkey) =>
Button({
key: 'tab-' + name,
label,
hotkey,
plain: true,
// Dim the tab that isn't open
dimColor: tab !== name,
onPress: () => {
tab = name
redraw()
},
})
// What goes under the tabs, depending on which one is open
const body =
tab === 'one'
? [Text({ children: ['This is the first tab.'] })]
: [
Box({
flexDirection: 'row',
columnGap: 2,
children: [
Button({
key: 'more',
label: 'Add one',
hotkey: 'a',
onPress: async () => {
count += 1
redraw()
// Save the count so it's there after a restart
await $.store.set('count', count)
},
}),
Text({ children: ['Count: ' + count] }),
],
}),
]
// The whole pane: the row of tabs, a blank line, then the body
return Box({
flexDirection: 'column',
children: [
Box({
flexDirection: 'row',
columnGap: 3,
children: [tabButton('one', 'One', '1'), tabButton('two', 'Two', '2')],
}),
Text({ children: [' '] }),
...body,
],
})
})
}כל הוק גם עושה משהו שהקוד לא מבהיר:
session.startגם קורא את הספירה השמורה מ-$.store, מאגר של מפתח וערך שנשמר בין סשנים.command.runרק אומר ל-Claude Code שהחלונית קיימת. פתיחת חלונית לא מציירת כלום בפני עצמה: Claude Code מפעיל אחר כך אתui.renderכדי לשאול מה נכנס אליה.ui.renderמחזיר את עץ האלמנטים,Boxשמחזיק תיבות אחרות, טקסט וכפתורים, ובונה אותו מחדש מ-tabו-countבכל פעם שהוא רץ.
לחיצה על כפתור מריצה את ה-callback onPress שלו, שמשנה משתנה וקורא ל-redraw. Claude Code מריץ אחר כך את הוק ui.render שוב, וההוק בונה עץ חדש מהערכים החדשים. כל ציור אינטראקטיבי משתמש במחזור הציור הזה: callback משנה מצב, וההוק מצייר שוב מהמצב החדש.
3. פתיחת החלונית
במעטפת שלך, הפעל את Claude Code עם claude --plugin-dir ./hello-tabs. בפרומפט של Claude Code, הרץ /hello-tabs. נפתחת חלונית עם 1: One ו-2: Two לרוחב החלק העליון. לחץ 2, ואז לחץ a, מקש הקיצור של Add one, כמה פעמים. הספירה עולה.
4. בדיקה שהספירה נשמרה
לחץ Esc כדי לסגור את החלונית, ואז צא מהסשן. במעטפת שלך, הפעל את Claude Code שוב עם אותה פקודה claude --plugin-dir ./hello-tabs, ובפרומפט של Claude Code הרץ /hello-tabs. הספירה נמצאת איפה שהשארת אותה.
כדי לנקות את הספירה, גרום למוד לקרוא ל-$.store.delete('count'). שמירת מצב מכסה כמה זמן כל סוג של ערך נשמר.
#בחירה איפה לצייר
הוק ui.render רץ לכל אתר ציור, אלא אם מצמצמים אותו לאתר שבו רוצים לצייר. כדי לבחור את אתר הציור, מעבירים מסנן, שנקרא matcher, כארגומנט השני של on. { component: 'Pane' } מריץ את ההוק רק עבור panes. בתוך ההוק, e.component נותן את שם האתר, e.surface אומר איזו אפליקציה מציירת, ו e.props מחזיק את הנתונים של האתר עצמו. עבור pane, e.requestId הוא ה id שאיתו פתחתם אותו.
ה pane והפס ריקים עד שמוד ממלא אותם. בחרו לשונית כדי לראות מה כל אחד מהם ואיך לצייר בו:
Pane
pane הוא סרגל צד ליד התמליל בטרמינל מסך מלא רחב, או אזור עם מסגרת מעל ה prompt במקרים אחרים. כשכמה panes פתוחים, כל אחד מקבל לשונית שמציגה את הכותרת שלו.
pane מופיע כשהמוד שלכם קורא ל $.ui.open עם id שאתם בוחרים, כמו ב $.ui.open({ id: 'hello-tabs' }). פתיחת pane בזמן הנכון מכסה את השדות האחרים ומתי pane ממתין לטרמינל רחב יותר.
כדי לצייר ב pane שלכם, סננו לפי { component: 'Pane' } ובדקו ש e.requestId הוא ה id שלכם.
הפס מעל ה prompt
הפס הוא רצועה ישירות מעל שדה הקלט של ה prompt. הוא תמיד שם, וכל המודים חולקים אותו.
ההוק שלכם מחזיר עץ כדי להציג משהו בפס, או next(e) כדי לא להציג כלום. עץ מחליף את מה שהמודים שאחרי שלכם מציירים שם. כדי לשמור את שלהם, שימו את התוצאה של await next(e) בין הילדים של Box בעץ שלכם.
כדי לצייר בפס, סננו לפי { component: 'AbovePrompt' }.
#שינוי מה ש Claude Code כבר מצייר
Claude Code מצייר את רוב הממשק שלו בעצמו: הודעות, שורות של קריאות כלים, ה spinner, ועוד. כל אחד מהחלקים האלה הוא גם אתר ציור, כך שמוד יכול לעצב אותו מחדש או להחליף אותו. כדי לשנות אחד מהם, סננו את הוק ui.render לפי השם שלו מהטבלה הזו:
| אתר | מה זה |
|---|---|
UserMessage, AssistantMessage | הודעה בתמליל |
ToolUse, ToolResult, ToolGroup | השורה של קריאת כלי, התוצאה שלה, וקבוצה מכווצת של קריאות |
CommandOutput | השורה שפקודה הדפיסה |
AskUserQuestion | תיבת הדו שיח ש Claude פותח כדי לשאול אתכם שאלה |
Spinner, ToolProgress, TurnDuration | שורות סטטוס של תור: השורה שמונפשת בזמן ש Claude עובד, שורת ההתקדמות החיה של כלי שרץ, והשורה שסוגרת תור |
InfoNotice, SessionMode, PromptHint | שורות סטטוס מתחת ללוגו, תוויות המצב בכותרת התחתונה, ושורת הרמז מתחת ל prompt |
Pane, AskUserQuestion, Spinner, UserMessage, AssistantMessage, ואתרי קריאות הכלים עובדים בטרמינל, באפליקציית Desktop, ובתוסף VS Code. טבלת אתרי הציור מפרטת איפה כל אחד מהאחרים מועלה.
באתר ש Claude Code כבר מצייר, ההוק שלכם יכול לשנות פרט, להחליף את הציור, או להשאיר אותו כמו שהוא. בחרו לשונית כדי לראות כל אחת מהאפשרויות על ה spinner. הדוגמאות קוראות משתנה calls שהוק אחר סופר, כמו במוד המדריך.
שינוי פרט
כדי לשמור את הציור של Claude Code ולשנות חלק אחד ממנו, העבירו ל next עותק של האירוע עם props ששונו. ההוק הזה משנה את הטקסט שאחרי המילה של ה spinner:
on('ui.render', { component: 'Spinner' }, async ($, e, next) => {
// Keep Claude Code's spinner, and change the text after its word
return next({ ...e, props: { ...e.props, suffix: ' · tool calls: ' + calls + '…' } })
})ה spinner שומר את האנימציה שלו ואת המילה שלו, והטקסט שלכם בא אחרי המילה:
Thinking · tool calls: 2…החלפת הציור
כדי לצייר משהו משלכם במקום האתר, החזירו עץ ואל תקראו ל next. ההוק הזה מצייר שורת טקסט אחת במקום שבו ה spinner היה מופיע:
on('ui.render', { component: 'Spinner' }, async ($, e) => {
const { Text } = $.ui.resolve(e)
// No call to next, so this line is drawn in the spinner's place
return Text({ children: ['Claude has made ' + calls + ' tool calls'] })
})בזמן ש Claude עובד, השורה שלכם מוצגת וה spinner של Claude Code לא:
Claude has made 2 tool callsהשארה כמו שהוא
כדי להשאיר את האתר כפי ש Claude Code מצייר אותו, החזירו next(e). הוק עושה זאת לעתים קרובות עבור חלק מהאירועים ולא עבור אחרים. ההוק הזה משאיר את ה spinner כמו שהוא עד שיש קריאה לספור:
on('ui.render', { component: 'Spinner' }, async ($, e, next) => {
// Nothing to show yet, so pass the event on unchanged
if (calls === 0) return next(e)
return next({ ...e, props: { ...e.props, suffix: ' · tool calls: ' + calls + '…' } })
})לפני קריאת הכלי הראשונה, ה spinner נראה כפי שהוא נראה בלי המוד:
Thinking…באתרים האלה, next(e) מחזיר הפניה לציור של Claude Code, { type: 'engine', ref }, אלא אם מוד שרץ אחרי שלכם החזיר עץ משלו. כדי לשנות את מה שבציור הזה, העבירו ל next עותק של האירוע עם props שונים, כמו שעושה הלשונית שינוי פרט. אפשר להחזיר את ההפניה כמו שהיא, או למקם אותה ב Box לצד אלמנטים משלכם:
on('ui.render', { component: 'Spinner' }, async ($, e, next) => {
const { Box, Text } = $.ui.resolve(e)
const theirs = await next(e)
return Box({ flexDirection: 'column', children: [theirs, Text({ children: ['under the spinner'] })] })
})בזמן ש Claude עובד, ה spinner מונפש כמקודם, ו under the spinner מופיע מתחתיו.
בקשת ההרשאה אינה אתר ציור, ולכן מוד לא יכול לשנות את מה שהיא מציגה. תיבת השאלה, AskUserQuestion, היא אתר ציור, ולכן מוד יכול לשנות אותה. עץ לתיבת הדו שיח חייב להחזיק את ההפניה בדיוק פעם אחת, עם האלמנטים שלכם מעליה. אחרת, Claude Code מצייר את תיבת הדו שיח שלו.
#פתיחת pane בזמן הנכון
pane מופיע רק כשהמוד שלכם פותח אותו. איך ומתי פותחים אותו קובע אם הוא לוקח מיקוד מקלדת, כמה מקום הוא מבקש, ואם הוא מוצג בכלל בטרמינל צר.
כדי לפתוח pane, קראו ל $.ui.open עם id שאתם בוחרים. ה id הוא שם ה pane: הוק ui.render שלכם בודק אותו, ומעבירים אותו שוב כדי לסגור את ה pane.
await $.ui.open({ id: 'hello-tabs', title: 'Hello tabs', focus: true })כדי לסגור את ה pane, קראו ל $.ui.close עם ה id שאיתו פתחתם אותו:
await $.ui.close({ id: 'hello-tabs' })מלבד id, $.ui.open מקבל את השדות האופציונליים האלה:
| שדה | מה הוא עושה |
|---|---|
title | תווית הלשונית של ה pane כשפתוח יותר מ pane אחד |
focus | מבקש מיקוד מקלדת |
closeOnEscape | גורם ל Esc לסגור את ה pane |
holdToasts | בטרמינל, מחזיק toasts בזמן שה pane הזה הוא המוצג. ראו החזקת toasts מאחורי תיבת דו שיח. |
rows | הגובה לבקש כשה pane יושב מעל ה prompt. ברירת המחדל היא שליש מהמקום. |
columns | הרוחב לבקש כשה pane יושב לצד התמליל, לכל היותר 45% מרוחב הטרמינל. אחרי שהמשתמש שינה גודל של pane לצד התמליל, בסשן הזה או בסשן קודם, panes נפתחים שם ברוחב שהמשתמש קבע במקום זאת. אותה מגבלת 45% חלה על הרוחב הזה. |
תוסף VS Code מתעלם מ rows ומ columns וקובע את גודל ה pane בעצמו.
focus, closeOnEscape, ו holdToasts אופציונליים ומקבלים רק true. כדי להשאיר אחד מהם כבוי, השמיטו אותו. העברת false זורקת שגיאה כגון ui.open: focus is true or left out. כדי להגדיר אחד מהם בתנאי, הוסיפו את השדה רק כשהתנאי מתקיים. הקריאה הזו מבקשת מיקוד מקלדת רק כש items אינו ריק:
const pane = { id: 'hello-tabs', title: 'Hello tabs' }
await $.ui.open(items.length > 0 ? { ...pane, focus: true } : pane)כדי לאפשר לפקודה לפתוח את ה pane בזמן ש Claude עובד, הוסיפו immediate: true כשאתם רושמים את הפקודה. בלעדיו, פקודה שהוקלדה במהלך תור ממתינה לסיום התור.
#החזקת toasts מאחורי תיבת דו שיח
העבירו holdToasts: true ל $.ui.open כשה pane הוא תיבת דו שיח שהמשתמש עונה לה ועוזב, כדי ש toasts לא יופיעו בזמן שהוא מחליט. בטרמינל, ההחזקה נמשכת כל עוד ה pane הזה הוא המוצג, ו toast שעולה בזמן הזה ממתין עד שההחזקה מסתיימת.
Claude Code מחזיק toasts של מודים אחרים והתראות קצרות טווח משלו, וגם את אלה שהמוד שלכם מעלה עם $.ui.toast. השאירו את השדה מחוץ ל pane שנשאר פתוח, כדי שהמשתמש ימשיך לראות אותם.
#כש pane ממתין לטרמינל רחב יותר
pane שהמוד שלכם פותח בלי שהתבקש אינו מופיע בטרמינל צר, ולכן הוא לא יכול להשתלט על מסך קטן. אם הוא מופיע תלוי במה שפתח אותו:
- נפתח על ידי משהו שהמשתמש עשה, כגון פקודה שהריץ או כפתור שלחץ, ה pane מופיע בכל רוחב
- נפתח על ידי המוד שפועל מעצמו, כגון מטיימר או מהוק
turn.start, ה pane מופיע רק בטרמינל ברוחב של 144 עמודות לפחות. אחרי שהמשתמש פתח את ה pane הזה פעם אחת בעצמו, 110 עמודות מספיקות.
כשה pane מופיע, $.ui.open מתממש אל { isPlaced: true }. כשה pane ממתין, isPlaced הוא false ו reason הוא מחרוזת שאומרת למה. pane שממתין מופיע כשהמשתמש פותח אותו או מרחיב את הטרמינל. כדי לומר שמשהו זמין בלי לפתוח pane, קראו ל $.ui.toast('Your message'), שמראה התראת toast.
#ציור בתוסף VS Code
בתוסף VS Code, מה שמוד מצייר מופיע בלוח הצ'אט עם גרסת תוסף 2.1.299 או מאוחר יותר. סשן שלא שולף דגלי תכונות, כגון סשן על Amazon Bedrock, צריך 2.1.304 או מאוחר יותר. בהוק ui.render עבור הלוח, e.surface הוא vscode. כדי לבדוק אילו אתרים ואלמנטים הלוח מצייר, ואילו props הוא מתעלם מהם, ראו את טבלאות אתרי הציור והאלמנטים.
כדי שמוד יעבוד בלוח וגם בטרמינל, קחו בחשבון את ההבדלים האלה:
- Panes: בלוח של 800 פיקסלים או יותר, panes נעגנים לצד השיחה,
e.props.placementהוא'dock', וe.viewport.isFullscreenהואtrue. בלוח צר יותר, panes מצוירים מעל תיבת ה prompt,placementהוא'inline', ו pane מציג 12 שורות לפני שהוא גולל. - Toasts ושורות סטטוס: אלה יושבים בערימה אחת ישירות מעל תיבת ה prompt, יחד עם ה panes כשהלוח צר, עם שורת סטטוס אחת לכל מוד. הערימה מוסתרת בזמן שמוצגת בקשת הרשאה או תיבת דו שיח.
- תוויות: הלוח ממסגר את מה שמוד מצייר או משכתב ומתייג אותו בשם התוסף, כמו From the token-chart plugin או Edited by the token-chart plugin.
- מקלדת: לכפתורים אין מקשי קיצור, ולכן המשתמש לוחץ על כפתור או לוחץ Enter עליו אחרי שיש לו מיקוד.
- מיקוד מקלדת: הלוח נענה לבקשת
focus: trueשל pane רק כשללוח יש מיקוד, Claude פנוי בלי שאלה שממתינה, ותיבת ה prompt ריקה. - תיבת השאלה: ב
AskUserQuestion, הלוח שומר את כרטיס השאלה שלו ומצייר את האלמנטים שלכם מעליו. props שמעבירים לnextלא משנים את הכרטיס. - הצעות ל prompt:
$.prompt.suggestמתממש עםisShown: falseולא מציג כלום, כי הלוח שומר את טקסט ההצעה המעומעם של תיבת ה prompt להצעות של Claude עצמו. - מגבלות טקסט: הלוח חותך שורת סטטוס ב 200 תווים או שתי שורות, ו toast ב 500 תווים.
#בניית עץ מאלמנטים
מה שהוק ui.render מחזיר הוא עץ אלמנטים: תיאור של מה לצייר, עשוי מתיבות, טקסט ופקדים שמקוננים זה בתוך זה. אתה מתאר את הציור, ו-Claude Code מרנדר אותו בכל אפליקציה שהמשתמש פתח: הטרמינל, אפליקציית Desktop, או התוסף של VS Code.
כדי לקבל את האלמנטים, קרא ל-$.ui.resolve(e) בהוק שלך, כמו ב-const { Box, Text, Button } = $.ui.resolve(e). כל אלמנט הוא פונקציה. מעבירים לו props, ואת האלמנטים והמחרוזות שנכנסים לתוכו שמים ב-children.
בחר לשונית כדי לראות כל אחד מהאלמנטים הנפוצים ביותר, ואיך הטרמינל מצייר אותו:
#Text
Text מצייר מחרוזת, עם עיצוב אופציונלי כמו bold ו-color:
Text({ children: ['This is the first tab.'] })This is the first tab.#Box
Box מסדר את מה שבתוכו, בשורה או בעמודה. זה שם כפתור ושורה של טקסט זה לצד זה, במרווח של שתי עמודות:
Box({
flexDirection: 'row',
columnGap: 2,
children: [
Button({ key: 'more', label: 'Add one', onPress: addOne }),
Text({ children: ['Count: 0'] }),
],
})[ Add one ] Count: 0#Button
Button הוא פקד שהמשתמש יכול ללחוץ עליו. הוא מריץ את ה-callback onPress שלך. עם plain: true אין לו סוגריים, והוא מציג את מקש הקיצור שלו:
Button({ key: 'more', label: 'Add one', onPress: addOne })
Button({ key: 'tab-one', label: 'One', hotkey: '1', plain: true, onPress: showTabOne })[ Add one ]
1: One#Input
Input הוא שדה טקסט. הוא מריץ את ה-callback onSubmit שלך עם הטקסט כשהמשתמש לוחץ Enter:
Input({
key: 'new-note',
label: 'Note',
placeholder: 'Type a note and press Enter',
value: '',
submitLabel: 'add',
onSubmit: addNote,
})Note: Type a note and press Enterקבע את rows ל-2 או יותר עבור שדה שמכיל כמה שורות.
ב-גלריית הממשק יש דוגמאות וצילומי מסך של רוב האלמנטים. הטבלה הזו מפרטת כל אלמנט:
| אלמנט | מה הוא מצייר | איפה |
|---|---|---|
Box | מכל flex שמקבל props של פריסה כמו flexDirection, borderStyle, ו-width. props של מרווח כמו columnGap ו-padding מקבלים מספר או שם מדרגה מ-'xs' עד 'xl'. | בכל מקום |
Text | טקסט מעוצב. מקבל color, bold, dimColor, italic, wrap, ואחרים כמו weight. color הוא מפתח ערכת נושא או צבע כמו 'red'. wrap הוא 'wrap', 'truncate', 'truncate-start', 'truncate-middle', או 'truncate-end'. | בכל מקום |
Button | פקד שקורא ל-onPress | בכל מקום |
Link, Code, Markdown | קישור עם href ו-label אופציונלי, בלוק קוד, וטקסט שמעוצב כמו תשובות של Claude. Markdown מקבל את התוכן שלו ב-prop text, לא ב-children, וצריך key כשמעבירים onLinkPress. | בכל מקום |
Input, Select | שדה טקסט ותפריט נפתח | טרמינל, Desktop, VS Code |
Svg | מסמך SVG | Desktop, VS Code |
Client | אזור שמצויר על ידי קובץ שני שלך, להנפשה ולקלט מצביע. לקובץ הזה אין API של mods. הוא מגיע להוקים שלך על ידי שליחת נתונים, שמגיעים כאירוע ui.message. אם הוא נכשל בטעינה, בציור או בהרצה, ההוקים שלך מקבלים אירוע ui.fault. | טרמינל, Desktop |
Raster, Image | רשת של תאים צבעוניים, ותמונה | טרמינל |
אם המודול שלך הוא קובץ .tsx או .jsx, אפשר לכתוב את העץ כ-JSX. קודם מפרקים את האלמנטים מ-$.ui.resolve(e).
אם הציור שלך לא מופיע, בדוק בעץ prop שאלמנט לא מקבל, או ילד במקום שבו אף ילד לא נכנס. הוק שמחזיר עץ כזה נכשל, ושורת hook skipped מציינת את שם ה-mod שלך ואת הכלל שהעץ הפר. עבור Text בחלונית עם prop בשם bogusProp, השורה הזו היא:
my-mod: ui.render hook skipped: returned the wrong shape (a tree Pane refuses: Text prop "bogusProp" is not allowed)#Link באפליקציית Desktop וב-VS Code
באפליקציית Desktop ובתוסף VS Code, Link מצויר כטקסט רגיל, אלא אם ה-href שלו עומד בדרישות האלה:
- סכמה ומארח: כתובת
https:, או כתובתhttp://localhostכמוhttp://localhost:3000 - בלי
@: כתוב@בנתיב או בשאילתה כ-%40 - איות: מה ש-
new URL(href).hrefמחזיר, מלבד/חסר אחרי המארח. זה לא כולל מארח באותיות גדולות, רווח, ו-:443בכתובתhttps:.
בטרמינל הדרישות האלה לא חלות.
#כש-Client נכשל
בטרמינל, כשהקובץ ש-Client מריץ נכשל, שורה מעומעמת כמו my-mod: Client client/spinner.js: boom תופסת את מקומו של ה-Client, ושאר הציור שלך עדיין מוצג.
אם ה-mod שלך מטפל ב-ui.fault, Claude Code אחר כך מצייר את האתר מחדש.
#ציור רשת של תאים צבעוניים
עבור מפת חום, sparkline, או לוח משחק בטרמינל, צייר Raster אחד, ולא Box לכל תא. Raster מקבל key, את הגודל שלו ב-columns וב-rows, ו-cells, מחרוזת base64 שאורזת כל תא. כל תא הוא שלושה מספרים: נקודת הקוד של התו, הצבע שלו, וצבע הרקע שלו. צבע הוא ערך RGB של 24 סיביות בהקסדצימלי, כמו 0xc62828 לאדום. הערך 0x01000000, אחד מעל הטווח הזה, פירושו ברירת המחדל של הטרמינל.
באפליקציות שאינן הטרמינל אין Raster, לכן בדוק את e.surface וצייר שם טקסט. גוף החלונית הזה מצייר מפת חום של שלוש על שתיים:
// The value that means "use the terminal's default color"
const DEFAULT_COLOR = 0x01000000
// Pack rows of [character, color] pairs into the one string a Raster takes
// One cell is three numbers: the character's code point, its color, and its background
function cellsOf(rows) {
const numbers = rows.flat().flatMap(([char, color]) => [char.codePointAt(0), color, DEFAULT_COLOR])
return new Uint8Array(Uint32Array.from(numbers).buffer).toBase64()
}
on('ui.render', { component: 'Pane' }, async ($, e, next) => {
// Draw only in the pane opened with the id 'heat'
if (e.requestId !== 'heat') return next(e)
const { Box, Text, Raster } = $.ui.resolve(e)
// Two rows of three cells, each a block character and its color
const rows = [
[['█', 0x2e7d32], ['█', 0xf9a825], ['█', 0xc62828]],
[['█', 0x2e7d32], ['█', 0x2e7d32], ['█', 0xf9a825]],
]
if (e.surface !== 'terminal') {
return Text({ children: ['The heat map needs the terminal.'] })
}
return Box({
flexDirection: 'column',
children: [Raster({ key: 'grid', columns: 3, rows: 2, cells: cellsOf(rows) })],
})
})בטרמינל, החלונית מציגה את הרשת:
מערך rows הוא החלק שמשנים, ו-cellsOf הופך אותו למחרוזת הארוזה. ההוק מצייר רק בחלונית שה-id שלה הוא heat, לכן פתח אחת עם $.ui.open({ id: 'heat' }) מתוך פקודה, כמו שדוגמת hello-tabs פותחת את החלונית שלה.
כל תו חייב להיות ברוחב של תא אחד. כדי להנפיש Raster שכבר על המסך, קרא ל-$.ui.blit עם ה-id של החלונית כ-requestId, ה-key של ה-Raster, אותו גודל, ותאים חדשים. בדוגמה הזו זה $.ui.blit({ requestId: 'heat', key: 'grid', columns: 3, rows: 2, cells: cellsOf(newRows) }). זה צובע מחדש את האלמנט האחד הזה בלי להריץ שוב את הוק ui.render שלך.
#תגובה ללחיצות ולהקלדה
כשהמשתמש לוחץ על כפתור, מקליד בשדה, או בוחר מתוך רשימה שהמוד צייר, Claude Code קורא ל-callback של הפקד הזה, והוא רץ במודול שלך. לכל פקד יש callbacks משלו:
Button: מקבלonPress(e), כאשרe.surfaceהוא האפליקציה שממנה הגיעה הלחיצהInput: מקבלonSubmit(value)ו-onInput(value)Select: מקבלonSelect(value), והאפשרויות שלו נמצאות ב-options, רשימה של לפחות אפשרות אחת עם ערכים ייחודיים, למשל[{ value: 'sm', label: 'Small' }, { value: 'lg', label: 'Large' }]
#אילו פקדים צריכים key
תן לכל Input ולכל Select מאפיין key, למשל key: 'size'. Button יכול בלי key כשיש לו מאפיין label או ילד שהוא מחרוזת יחידה. ה-key שלו הוא אז ערך ה-label או המחרוזת הזאת.
בדיקות ומודים אחרים מזהים פקד לפי ה-key שלו:
- בדיקות: בדיקה לוחצת או מקלידה בפקד לפי ה-key שלו.
- מודים אחרים: Claude Code מפעיל
ui.press,ui.input, אוui.selectעם ה-key של הפקד ב-e.elementלפני שהוא מריץ את ה-callback של הפקד. hook של מוד אחר לאירוע הזה רואה מה המשתמש הקליד ב-Inputשלך, ויכול לשנות את הטקסט הזה או למנוע מה-callback שלך לרוץ. ה-API של המודים לא נותן למוד הזה מתודה ללחוץ על הכפתורים שלך.
אם הודעה אומרת needs a key או is drawn twice, מצא את הסיבה לפי ההודעה הזאת.
#מיקוד מקלדת ומקשי קיצור
המוד שלך אף פעם לא קורא את המקלדת בעצמו. המשתמש לוחץ על מקש, Claude Code מחליט לאיזה מהפקדים שלך המקש מיועד, וה-callback של הפקד הזה רץ. מלבד מקש קיצור ספרתי על הפס או קישור action, זה קורה רק בזמן שלחלונית או לפס שלך יש מיקוד מקלדת. בשאר הזמן, המקשים הולכים ל-prompt.
בתוסף VS Code, לכפתורים אין מקשי קיצור: המשתמש לוחץ על כפתור או לוחץ Enter עליו, ומקליד בשדות.
#איך חלונית מקבלת מיקוד מקלדת
חלונית מקבלת מיקוד מקלדת כאשר:
- המוד שלך פותח אותה עם
focus: trueמפקודה או מלחיצה - המשתמש לוחץ Ctrl+X ואז Tab
- המשתמש לוחץ עליה, ובטרמינל זה דורש עיבוד במסך מלא
Claude Code מעניק focus: true רק בזמן שה-prompt ריק ולשום דבר אחר אין מיקוד מקלדת. חלונית שנפתחת בזמן שהמשתמש מקליד לא לוקחת את ההקשות שלו.
#מה כל מקש עושה
הטבלה הזאת מפרטת מה מקש עושה בזמן שלחלונית או לפס שלך יש מיקוד מקלדת:
| מקש | מה הוא עושה |
|---|---|
| Tab | עובר לפקד הבא |
| Up ו-Down | עוברים בין פקדים בזמן שהציור שלך נכנס. כשלחלונית או לפס יש יותר שורות ממה שאפשר להציג, הם מגללים אותם. ב-Input בטרמינל, הם עוברים בין שורות השדה, ומהשורה הראשונה או האחרונה שלו הם עוברים לפקד שלפניו או לפקד שאחריו. |
| Enter | לוחץ על ה-Button שבמיקוד, שולח את ה-Input שבמיקוד, או בוחר ב-Select |
| מקש הקיצור של כפתור | לוחץ על הכפתור הזה. בזמן של-Input יש את המיקוד, כל מקש שניתן להדפיס הולך לשדה. |
| Page Up, Page Down, Home, ו-End | מגללים את החלונית או את הפס כשיש להם יותר שורות ממה שאפשר להציג |
| Ctrl+X ואז מקש חץ | משנה את גודל החלונית שלך. Left או Up נותנים לה יותר מקום, ו-Right או Down מחזירים את המקום. חלונית לצד התמליל מפסיקה להתרחב ב-45% מרוחב הטרמינל. |
| Ctrl+X ואז X | סוגר את החלונית שלך, גם בזמן שלאחד השדות שלה יש את המיקוד |
| Ctrl+X ואז Ctrl+A | מכווץ את הפס לשורה אחת בזמן שמוד מצייר שם, או מרחיב אותו שוב. המקשים האלה עובדים גם בזמן של-prompt יש את המיקוד. |
| Esc | מחזיר את מיקוד המקלדת ל-prompt. עם closeOnEscape: true, הוא גם סוגר את החלונית. |
מוד לא יכול לקשור את Tab או את מקשי החצים למשהו אחר, ולכן משחק מנווט עם w, a, s, ו-d.
#קביעת מקש קיצור והמיקוד הראשון
hotkey: כדי לאפשר למשתמש ללחוץ עלButtonבמקש אחד, תן לוhotkeyשל ספרה אחת או אות קטנה אחת, כמו ב-hotkey: 'a'autoFocus: כדי לבחור איזה פקד מקבל את המיקוד כשהחלונית נפתחת, הוסף לוautoFocus: true. בתוסף VS Code,autoFocusלא עושה כלום: הפקד מצויר כאילו השמטת אותו, ואין צורך לשנות כלום. המאפיין מקבל רקtrue, אז השמט אותו בשאר הפקדים.
האופן שבו מקש קיצור מוצג תלוי בכפתור ובאפליקציה:
| כפתור | בטרמינל | באפליקציית Desktop | בתוסף VS Code |
|---|---|---|---|
| עם סוגריים, ברירת המחדל | [ Add one ], בלי שמקש הקיצור מוצג | התווית עם מקש קטן לידה | התווית, ומקש הקיצור לא עושה כלום |
עם plain: true | 1: One | התווית עם מקש קטן לידה | התווית, ומקש הקיצור לא עושה כלום |
בטרמינל, ציין את המקש בתווית של כפתור עם סוגריים, או השתמש ב-plain: true, כדי שהמשתמש יוכל לראות על מה ללחוץ. בהפניה לרכיבים נמצאים שאר כללי ה-Button: action, מקשי קיצור ספרתיים על הפס, ושני כפתורים על מקש קיצור אחד.
#כשהמשתמש מכווץ את הפס
בטרמינל, בזמן שמוד מצייר בפס, המשתמש יכול לכווץ את הפס לשורה אחת כדי להזיז אותו מהדרך:
- לחיצה על
[-]: בזמן שהפס מורחב, Claude Code מצייר את הסימן הזה בקצה הימני של השורה הראשונה שלו. בעיבוד במסך מלא, המשתמש לוחץ עליו כדי לכווץ את הפס. - לחיצה על Ctrl+X ואז Ctrl+A: זה עובד בין אם ל-
prompt, לפס, או לחלונית יש מיקוד מקלדת. המשתמש יכול לקשור מקשים אחרים אליו דרך הפעולהabovePrompt:toggle.
פס מכווץ מסתיר את כל מה שהמודים מציירים שם ומציג במקום זאת שורה עמומה אחת, שמציינת את המקשים שמרחיבים אותו כפי שהמשתמש קשר אותם:
▸ plugin panel hidden · ctrl+x ctrl+a or click to showאותם מקשים, או בעיבוד במסך מלא לחיצה על השורה, מרחיבים את הפס שוב. עד אז, שום דבר לא לוחץ על כפתור בו: אין מקש קיצור, אין ספרה שהוקלדה לתוך prompt ריק, ואין keybinding ל-action של כפתור. אם לפס יש מיקוד מקלדת כשהמשתמש מכווץ אותו, המיקוד חוזר ל-prompt, ו-Ctrl+X ואז Tab מדלגים על הפס בזמן שהוא מכווץ.
הפס נשאר מכווץ עד שהמשתמש מרחיב אותו, גם אחרי /clear, והוא מורחב שוב בפעם הבאה ש-Claude Code מתחיל. המוד שלך לא יכול להרחיב אותו או לדעת שהוא מכווץ: ה-hook של ui.render ממשיך לרוץ, ושום דבר ב-e.props לא אומר זאת. אל תהפוך את הפס למקום היחיד שבו המוד שלך מציג משהו שהמשתמש חייב לענות עליו.
#קבלת קלט מוקלד וציור שורה לכל פריט
חלוניות רבות הן שדה טקסט עם רשימה מתחתיו. הדוגמה בקטע הזה היא חלונית הערות: מקלידים הערה ולוחצים Enter כדי להוסיף אותה, ולכל הערה יש כפתור x שמוחק אותה. עם שתי הערות שנוספו, הטרמינל מצייר את החלונית כך:
╭────────────────────────────────────────────────────────✕─╮
│ Note: Type a note and press Enter ⏎ add │
│ x buy milk │
│ x call bob │
╰──────────────────────────────────────────────────────────╯ה-✕ על המסגרת העליונה הוא הסימן של Claude Code עצמו לסגירת החלונית.
הדוגמה משתמשת בטכניקות האלה:
- קבלת קלט מוקלד:
Inputקורא ל-onSubmit(value)עם הטקסט של השדה כשהמשתמש לוחץ Enter, ול-onInput(value)בכל שינוי - ציור רשימה: ממפים את הנתונים לשורה אחת לכל פריט, ונותנים לכפתור של כל שורה
keyמשלו
ה-hook הזה מצייר את תוכן החלונית:
// The list the pane draws
let notes = []
on('ui.render', { component: 'Pane' }, async ($, e, next) => {
// Draw only in the pane opened with the id 'notes'
if (e.requestId !== 'notes') return next(e)
const { Box, Text, Button, Input } = $.ui.resolve(e)
const redraw = () => $.ui.invalidate('ui.render')
return Box({
flexDirection: 'column',
children: [
Input({
key: 'new-note',
label: 'Note',
placeholder: 'Type a note and press Enter',
// The text the field starts with
value: '',
submitLabel: 'add',
autoFocus: true,
// Runs when you press Enter in the field
onSubmit: async (value) => {
// Ignore an empty line
if (!value.trim()) return
notes = [...notes, value.trim()]
redraw()
await $.store.set('notes', notes)
},
}),
// One row for each note: a delete button, then the note's text
...notes.map((note, i) =>
Box({
flexDirection: 'row',
columnGap: 1,
children: [
Button({
// A key of its own, so each row's button can be told apart
key: 'delete-' + i,
label: 'x',
plain: true,
onPress: async () => {
notes = notes.filter((_, j) => j !== i)
redraw()
await $.store.set('notes', notes)
},
}),
Text({ children: [note] }),
],
}),
),
],
})
})כדי לנסות את החלונית:
- הוספת הערה: הקלד שורה ולחץ Enter. השורה מופיעה כשורה חדשה, והשדה מתרוקן.
- מחיקת הערה: לחץ Tab עד שלכפתור ה-
xשל ההערה יש את המיקוד, ואז לחץ Enter. ה-xהוא התווית של הכפתור ולא מקש קיצור, ולכן הקלדת האות לא לוחצת עליו.
כל שינוי עוקב אחרי אותו מחזור render כמו hello-tabs: ה-callback משנה את notes, קורא ל-redraw, ושומר את הרשימה ב-$.store. אם ה-hook שלך מצייר את החלונית שוב בזמן שהמשתמש מקליד הערה, השדה שומר את הטקסט שלו כל עוד ה-hook מעביר את אותו value בכל פעם. כדי להחליף את הטקסט שלו בטקסט משלך, העבר value שונה.
הדוגמה שומרת את ההערות ולא טוענת אותן. כדי להחזיר אותן בסשן הבא, קרא אותן ב-hook של session.start, באופן שבו hello-tabs קורא את count.
המאפיינים האלה מרכיבים את שורת השדה, Note: Type a note and press Enter ⏎ add:
| מאפיין | בדוגמה | מה זה |
|---|---|---|
label | Note | הטקסט לפני השדה. הטרמינל מצייר : אחריו. |
placeholder | Type a note and press Enter | טקסט עמום שמוצג בזמן שהשדה ריק |
value | '' | הטקסט שהשדה מתחיל איתו. כאן הוא ריק, ולכן ה-placeholder מוצג. |
submitLabel | add | המילה אחרי ⏎ שאומרת מה Enter עושה |
שליחה של Input לא מתחילה תור אלא אם ה-callback שלך קורא ל-$.prompt.submit.
#ציור מחדש של אתר
ציור הוא תמונת מצב: הוא מראה מה ה-hook של ui.render החזיר בפעם האחרונה שה-hook רץ. כדי להציג משהו חדש, ה-hook צריך לרוץ שוב. Claude Code מריץ אותו שוב עבור חלק מהשינויים, והמוד שלך מבקש את השאר.
#מתי Claude Code מצייר מחדש בלי שביקשו
Claude Code מריץ את ה-hook של ui.render שלך שוב כשמאפייני האתר משתנים או כשרוחב הטרמינל משתנה. כש-Client באתר נכשל והמוד שלך מטפל ב-ui.fault, Claude Code מריץ את ה-hook פעם נוספת אחרי שה-hooks של ui.fault שלך חוזרים, כדי שה-hook של ui.render שלך יוכל להשאיר את ה-Client בחוץ. הוא לא מריץ את ה-hook לפי טיימר, והוא לא יכול לדעת מתי משתנה במודול שלך משנה ערך.
#ציור מחדש כשהנתונים שלך משתנים
כדי שהאתרים שלך יצוירו שוב אחרי שהנתונים שלך משתנים, קרא ל-$.ui.invalidate('ui.render'). החלונית הזאת סופרת לחיצות. ה-callback של הכפתור משנה את count, ואז מבקש ציור מחדש:
let count = 0
on('ui.render', { component: 'Pane' }, async ($, e, next) => {
if (e.requestId !== 'counter') return next(e)
const { Box, Text, Button } = $.ui.resolve(e)
return Box({
flexDirection: 'row',
columnGap: 2,
children: [
Button({
key: 'more',
label: 'Add one',
onPress: () => {
count += 1
// The data changed, so ask Claude Code to draw the pane again
$.ui.invalidate('ui.render')
},
}),
Text({ children: ['Count: ' + count] }),
],
})
})כל לחיצה מעלה את המספר בחלונית. דוגמת hello-tabs עוטפת את אותה קריאה בפונקציית redraw שלה.
ערך שאתה שומר ב-$.state לא צריך את הקריאה, כי כתיבת הערך מציירת מחדש את האתרים שקוראים אותו.
#ציור מחדש לפי טיימר
כדי ששעון, ספירה לאחור, או ערך שמגיע מחוץ לסשן יישארו עדכניים, צייר מחדש לפי לוח זמנים. התחל טיימר ב-hook של session.start של המודול. אם למודול כבר יש אחד, כמו אצל hello-tabs, הוסף אליו את השורה $.clock.every:
on('session.start', async ($, e, next) => {
// Every 1000 milliseconds, ask Claude Code to draw your sites again
$.clock.every(1000, () => $.ui.invalidate('ui.render'))
return next(e)
})Claude Code מריץ עכשיו את ה-hook של ui.render שלך פעם בשנייה. הטיימר נעצר אחרי שהמודול נטען מחדש, והמופע החדש של המודול מתחיל טיימר משלו.
#כל כמה זמן אתר יכול להצייר מחדש
Claude Code מגביל את קצב הציור מחדש של אתר, כך שהמוד שלך יכול לקרוא ל-$.ui.invalidate בתדירות שבה הנתונים שלו משתנים. למידע על כל כמה זמן כל אתר יכול להצייר מחדש, ראה את טבלת המגבלות.
קריאות שמגיעות מהר יותר מהמגבלה מתמזגות לציור מחדש אחד. הציור מחדש הזה מריץ את ה-hook שלך פעם אחת, וה-hook קורא את הנתונים שלך כפי שהם ברגע הזה, כך שהערך האחרון מוצג והערכים שביניהם לא. אנימציה לא יכולה לרוץ מהר יותר מהמגבלה.
#שמירה על מצב
המקום שבו מוד שומר ערך קובע כמה זמן הערך נשמר: עד שהמודול נטען מחדש, עד שהסשן מסתיים, או מסשן אחד לבא אחריו. בחרו לפי כמה זמן הערך צריך להישמר:
| שמרו אותו ב | הוא נשמר עד | השתמשו בו עבור |
|---|---|---|
| משתנה ברמת המודול | המודול נטען מחדש, וזה קורה בכל פעם ששומרים קובץ במהלך הפיתוח | ערכים שאפשר לאבד, כמו ש-tab נמצא ב-hello-tabs |
$.state | הסשן מסתיים, או שהמשתמש מריץ /clear, /resume או /branch | ערכים שציור תלוי בהם ושצריכים לשרוד טעינה מחדש |
$.store | המוד שלכם מוחק אותו, או שאף סשן לא קורא או כותב ל-store במשך cleanupPeriodDays. ה-store הוא מאגר מפתח-ערך, שנשמר כקובץ JSON של התוסף שלכם תחת ~/.claude/plugins/store/. | הגדרות, היסטוריה, כל דבר שהמשתמש מצפה למצוא בפעם הבאה |
$.store.get(key) מתפרש לערך או ל-undefined, ו-$.store.set(key, value) מקבל כל ערך JSON.
#שמירת ערך ב-$.state
$.state מחזיק ערכים למשך הסשן, והוא מצייר מחדש עבורכם. זה מצב ריאקטיבי: hook של ui.render שקורא ערך נרשם אליו, כך ש-Claude Code מצייר מחדש את האתר הזה בכל פעם שכותבים את הערך, ואין צורך לקרוא ל-$.ui.invalidate. ערך ב-$.state גם שורד טעינה מחדש של המודול, בניגוד למשתנה.
כדי להגדיר את זה, הכריזו על הערכים, כוונו את המניפסט להצהרה, ואז הגדירו והשתמשו בכל ערך. הדוגמאות מעבירות את count מ-hello-tabs אל $.state.
#הכרזה על הערכים
הכריזו על הערכים בקובץ הצהרת טיפוסים. המפתח החיצוני הוא שם התוסף שלכם, וכל רשומה תחתיו היא ערך והטיפוס שלו. שמרו את זה כ-hello-tabs/types/index.d.ts:
declare module 'claude-code' {
interface PluginState {
'hello-tabs': {
tab: 'one' | 'two'
count: number
}
}
}#כיוון המניפסט להצהרה
כדי ש-claude plugin validate יבדוק את הקוד שלכם מול הקובץ הזה, הוסיפו שדה types למניפסט עם הנתיב שלו:
{
"name": "hello-tabs",
"version": "0.1.0",
"description": "Opens a pane with two tabs and a counter",
"author": { "name": "Your Name" },
"types": "./types/index.d.ts"
}#הגדרה, קריאה וכתיבה של ערך
במודול שלכם, הגדירו כל ערך עם ברירת מחדל, קראו אותו בזמן הציור, וכתבו אותו מתוך callback. atom נותן שם לערך ולברירת המחדל שלו, read מחזיר אותו, ו-update כותב אותו. שלושת העזרים קוראים ל-$.state.get ול-$.state.set עבורכם:
import { atom, read, update } from 'claude-code'
// At the top of the module: name the value and give its default
const count = atom({ plugin: 'hello-tabs', key: 'count' }, 0)
// In the ui.render hook: read the value to draw it
const n = await read($, count)
// In a Button: write a new value from the old one
onPress: () => update($, count, (value) => value + 1)מכיוון שה-hook של ui.render קרא את count, Claude Code מריץ את ה-hook שוב בכל פעם שהכפתור כותב אותו.
הכללים האלה חלים על הקוד:
- כתבו את
pluginואתkeyכליטרלים של מחרוזת:claude plugin validateקורא אותם מהמקור שלכם - החזיקו את התוצאה של כל קריאה ל-
atomב-const: אם מכריזים עלcountעםlet, האימות נכשל עםtakes a source the scan can read - הכריזו על כל ערך בקובץ הצהרת הטיפוסים: אחרת האימות נכשל עם
hello-tabs.count is not declared - כתבו מתוך callback או מתוך hook של אירוע אחר: hook של
ui.renderיכול לקרוא מצב ואינו יכול לכתוב אותו, ולכן כותבים מתוךonPress,onSubmit, או hook לאירוע אחר
#שינוי hello-tabs כך שישתמש ב-$.state
כדי להעביר את count ב-hello-tabs אל $.state, שנו כל שורה שמשתמשת בו:
- בראש המודול: הוסיפו את שורת ה-
import, והחליפו אתlet count = 0בשורת ה-atom - ב-hook של
ui.render: הוסיפו את שורת ה-readלפניtabButton, וציירו'Count: ' + nב-Text - בכפתור Add one: החליפו את
onPressבזה שב-שמירה מיותר מסשן אחד, ששומר את ה-count וגם כותב אותו - ב-hook של
session.start: החליפו את שתי השורות שקוראות אתsavedבקריאה ל-loadCountמ-טעינת ערך שמור שוב אחרי/clear
השאירו את redraw עבור כפתורי הטאב, כי tab עדיין משתנה.
#טעינת ערך שמור שוב אחרי /clear
אם המוד שלכם מעתיק ערך שמור מ-$.store אל $.state ב-session.start, הוא צריך להעתיק אותו שוב אחרי /clear, /resume או /branch. הפקודות האלה מאפסות כל ערך ב-$.state לברירת המחדל שלו, ו-session.start לא נורה שוב. classic.SessionStart כן נורה אחרי כל אחת מהן, כש-e.source מוגדר ל-clear, resume או fork, ולכן העתיקו את הערך שוב ב-hook עליו. אחרת הציור שלכם מציג את ברירת המחדל, ו-callback ששומר את ערך ה-$.state כותב את ברירת המחדל מעל מה ששמרתם.
הקוד הזה טוען את count משני ה-hooks. הוא נשען על גרסת ה-$.state של hello-tabs, שבה count הוא atom ו-update מיובא. שימו את loadCount מעל register, והוסיפו את הקריאה ל-loadCount ל-hook של session.start שכבר יש לכם. classic.SessionStart גם נורה בהפעלה ואחרי compaction, וזה לא מאפס את $.state, ולכן הסינון על source מגביל את ה-hook לשלושת האיפוסים:
// Copy the saved count from $.store into $.state, or 0 if nothing is saved
async function loadCount($) {
const saved = Number((await $.store.get('count')) ?? 0)
await update($, count, () => saved)
}
// Runs before your first prompt, and again after a reload
on('session.start', async ($, e, next) => {
await loadCount($)
return next(e)
})
// Runs again after /clear, /resume, and /branch, which reports fork
on('classic.SessionStart', { source: ['clear', 'resume', 'fork'] }, async ($, e, next) => {
await loadCount($)
return next(e)
})כששני ה-hooks במקום, החלונית מציגה את ה-count השמור אחרי /clear ולא 0, והלחיצה הבאה על Add one מוסיפה ל-count השמור.
loadCount כותב את הערך השמור מעל זה שב-$.state, ו-session.start נורה שוב בכל פעם שהמודול נטען מחדש. כדי שה-store לא יישאר מאחור, שמרו בכל שינוי, כמו שכפתור Add one עושה.
כדי לבדוק את הטעינה מחדש בלי סשן, בדקו את הציור אחרי /clear.
#שמירה מיותר מסשן אחד
כל סשן במחשב שלכם שמריץ את המוד שלכם חולק $.store אחד. get שאחריו set אינו אטומי. כששני סשנים כל אחד קורא ערך, משנה אותו, וכותב אותו בחזרה, הם מתחרים, והכתיבה השנייה מחליפה את הראשונה.
כדי להקטין את הסיכוי לכך:
- תנו לכל פריט מפתח משלו:
setמשנה רק את המפתח שלו, כך שסשנים שכותבים מפתחות שונים לא דורסים זה את זה - קראו שוב ממש לפני שכותבים: לערך שכמה סשנים משנים, עשו
getלמפתח בתוך ה-callback ובנו את הערך החדש ממנו, לא מעותק שטענתם ב-session.start. כתיבה של סשן אחר עדיין אובדת אם היא נוחתת בין ה-getשלכם ל-setשלכם.
הכפתור הזה מוסיף אחד למה שה-store מחזיק עכשיו, ואז מעדכן את הציור:
onPress: async () => {
// Read what the store holds now, which another session may have changed
const saved = Number((await $.store.get('count')) ?? 0)
// Save the new count, then show it
await $.store.set('count', saved + 1)
await update($, count, () => saved + 1)
}אם סשן שני לחץ על הכפתור שלו שלוש פעמים מאז שהסשן הזה התחיל, הלחיצה הזו מציגה ושומרת count שכולל את שלוש הלחיצות האלה.
#הצעדים הבאים
- תגובה לאירועים: הזינו את הציור שלכם מקריאות כלים ומחורים
- שימוש ב-API של mods: הזינו את הציור שלכם מטיימרים ומקריאות מודל
- בדיקת ציור: לחצו על הכפתורים שלכם מבדיקה, ביותר ממשטח אחד
- אתרי רינדור ו-אלמנטים: ה-props של כל אתר וה-props של כל אלמנט